diff --git a/procedural/.gitignore b/procedural/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/procedural/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/procedural/NEXT_STEPS.md b/procedural/NEXT_STEPS.md
new file mode 100644
index 00000000..8c28f0b8
--- /dev/null
+++ b/procedural/NEXT_STEPS.md
@@ -0,0 +1,194 @@
+Excellent. The architectural backbone is now in place. We've successfully separated the *intent* (clicking "Generate" in the viewer) from the *execution* (running the pipeline in the engine).
+
+Now we address the errors you're seeing. These errors are expected and are our roadmap for the final stage of the engine refactoring. They are caused by functions in your new `engine/main.js` that were originally defined in the massive, global `main.js` (/Users/barrulus/Fantasy-Map-Generator/main.js) file.
+
+Our next task is to systematically move these remaining functions from `main.js` into their correct homes within the new engine structure, turning them into pure, importable modules.
+
+### The Strategy: Categorize and Relocate
+
+We will categorize the functions from `main.js` into logical groups and create new files for them, mostly within the `engine/utils/` directory, as they are largely helper functions for the main generation modules.
+
+Here is the breakdown and your next set of instructions.
+
+---
+
+### Step 1: Create a Home for Graph Utilities
+
+Many functions in `main.js` are related to the creation and manipulation of the Voronoi graph (`grid` and `pack`). The `graphUtils.js` you ported only contains a few of them. We need to create a more comprehensive module.
+
+**Your Action:**
+
+1. Create a new file: `src/engine/utils/graph.js`.
+2. Find the following functions in the **original `main.js` (/Users/barrulus/Fantasy-Map-Generator/main.js) file**, cut them out, and paste them into your new `graph.js` file.
+ * `generateGrid()`
+ * `placePoints()`
+ * `calculateVoronoi()`
+ * `getBoundaryPoints()`
+ * `getJitteredGrid()`
+ * `reGraph()`
+3. Refactor these functions to be pure ES modules:
+ * Add `export` before each function declaration.
+ * Remove all dependencies on global variables (`seed`, `graphWidth`, `graphHeight`, `grid`, `pack`, etc.). Pass them in as arguments.
+ * Import any necessary dependencies (like `Delaunator`, `Voronoi`, and other utils).
+ * Ensure they return their results instead of mutating global state.
+
+**Example for `generateGrid`:**
+
+```javascript
+// src/engine/utils/graph.js
+import { aleaPRNG } from './probability.js'; // Assuming this is where it will live
+import { Delaunator } from '../../libs/delaunator.js';
+import { Voronoi } from '../modules/voronoi.js';
+
+// Takes config, returns a new grid object
+export function generateGrid(config) {
+ const { seed, graphWidth, graphHeight } = config;
+ Math.random = aleaPRNG(seed);
+ const { spacing, cellsDesired, boundary, points, cellsX, cellsY } = placePoints(config);
+ const { cells, vertices } = calculateVoronoi(points, boundary);
+ return { spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed };
+}
+
+// placePoints needs config for graphWidth/Height and cellsDesired
+function placePoints(config) {
+ // ... logic ...
+ return { spacing, cellsDesired, boundary, points, cellsX, cellsY };
+}
+// ... and so on for the other functions
+```
+
+---
+
+### Step 2: Create a Home for Geographic and Climate Utilities
+
+These functions deal with the physical properties of the map, like temperature and coordinates.
+
+**Your Action:**
+
+1. Create a new file: `src/engine/utils/geography.js`.
+2. Find the following functions in the **original `main.txt` file**, cut them out, and paste them into your new file.
+ * `defineMapSize()`
+ * `calculateMapCoordinates()`
+ * `calculateTemperatures()`
+ * `generatePrecipitation()`
+ * `addLakesInDeepDepressions()`
+ * `openNearSeaLakes()`
+3. Refactor them:
+ * Add `export` to each function.
+ * Inject dependencies (`grid`, `pack`, `options`, `config`, other utils).
+ * Return new or modified data structures instead of mutating globals.
+
+**Example for `calculateTemperatures`:**
+
+```javascript
+// src/engine/utils/geography.js
+
+// It needs the grid, mapCoordinates, and temperature options from the config
+export function calculateTemperatures(grid, mapCoordinates, config) {
+ const cells = grid.cells;
+ const temp = new Int8Array(cells.i.length);
+ // ... existing logic ...
+ // ... use config.temperatureEquator etc. instead of options. ...
+
+ // for-loop to populate the `temp` array
+
+ // Return the new data
+ return { temp };
+}
+```
+
+---
+
+### Step 3: Create a Home for Population and Cell Ranking Utilities
+
+This is a key part of the generation logic that determines where cultures and burgs can settle.
+
+**Your Action:**
+
+1. Create a new file: `src/engine/utils/cell.js`.
+2. Find the `rankCells()` function in the **original `main.txt` file**, cut it out, and paste it into your new file.
+3. Refactor it:
+ * `export function rankCells(pack, grid, utils, modules)`
+ * It will need dependencies like `pack`, `grid`, `utils.d3`, and `modules.biomesData`.
+ * It should return an object containing the new `s` (suitability) and `pop` (population) arrays.
+ * `return { s: newSuitabilityArray, pop: newPopulationArray };`
+
+---
+
+### Step 4: Update the Engine Orchestrator (`engine/main.js`)
+
+Now that you've moved all these functions into modules, you need to update the orchestrator to import and use them correctly.
+
+**Your Action:** Modify `src/engine/main.js` to look like this.
+
+```javascript
+// src/engine/main.js
+
+// ... (existing module imports)
+
+// Import the new utility modules
+import * as Graph from "./utils/graph.js";
+import * as Geography from "./utils/geography.js";
+import * as Cell from "./utils/cell.js";
+import * as Utils from "./utils/index.js";
+
+export function generate(config) {
+ const timeStart = performance.now();
+ const { TIME, WARN, INFO } = Utils;
+ const seed = config.seed || Utils.generateSeed();
+ Math.random = Utils.aleaPRNG(seed);
+ INFO && console.group("Generating Map with Seed: " + seed);
+
+ // --- Grid Generation ---
+ let grid = Graph.generateGrid(config.graph);
+ grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils);
+ grid = Features.markupGrid(grid, config, Utils);
+ const { mapCoordinates } = Geography.defineMapSize(grid, config.map); // Now returns the coordinates object
+ grid = Geography.addLakesInDeepDepressions(grid, config.lakes, Utils);
+ grid = Geography.openNearSeaLakes(grid, config.lakes, Utils);
+
+ // --- Core Data Calculation ---
+ const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils);
+ grid.cells.temp = temp;
+ const { prec } = Geography.generatePrecipitation(grid, mapCoordinates, config.precipitation, Utils);
+ grid.cells.prec = prec;
+
+ // --- Pack Generation ---
+ let pack = Graph.reGraph(grid, Utils);
+ pack = Features.markupPack(pack, config, Utils, { Lakes });
+
+ // --- River Generation ---
+ const riverResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names });
+ pack = riverResult.pack;
+
+ // --- Biome and Population ---
+ const { biome } = Biomes.define(pack, grid, config.biomes, Utils);
+ pack.cells.biome = biome;
+ const { s, pop } = Cell.rankCells(pack, Utils, { biomesData: Biomes.getDefault() });
+ pack.cells.s = s;
+ pack.cells.pop = pop;
+
+ // --- Cultures, States, Burgs etc. (as before) ---
+ // ...
+
+ WARN && console.warn(`TOTAL GENERATION TIME: ${Utils.rn((performance.now() - timeStart) / 1000, 2)}s`);
+ INFO && console.groupEnd("Generated Map " + seed);
+
+ return { seed, grid, pack, mapCoordinates };
+}
+```
+**Note:** You will also need to update your `viewer/main.js` `buildConfigFromUI` function to create nested config objects for the new parameters (e.g., `config.graph`, `config.heightmap`, `config.temperature`). Use your existing `_config.md` located in /Users/barrulus/Fantasy-Map-Generator/procedural/src/engine/support/*_config.md files as a guide.
+
+### Your Goal for This Phase
+
+Your goal is to have a fully functional `engine/main.js` that can execute the entire generation pipeline without relying on *any* functions from the old `main.js`. After this step, `main.js` should be almost empty, containing only UI-specific logic (like event handlers, drawing functions, etc.), which we will deal with later.
+
+This is a significant undertaking. Take it one function at a time. The process is the same for each one:
+1. **Move** the function to its new home.
+2. **Export** it.
+3. **Identify** its dependencies.
+4. **Add** those dependencies to its argument list.
+5. **Return** its result instead of mutating globals.
+6. **Update** the caller (`engine/main.js`) to import and use the refactored function correctly.
+
+Report back when you have completed this. We will then be ready to connect the engine's output to the rendering system.
\ No newline at end of file
diff --git a/procedural/PORT_PLAN.md b/procedural/PORT_PLAN.md
new file mode 100644
index 00000000..63fcb83c
--- /dev/null
+++ b/procedural/PORT_PLAN.md
@@ -0,0 +1,277 @@
+# Port Project
+
+This project is decoupling a tightly-integrated browser application into a core procedural engine and a separate presentation layer. The goal of maintaining full cross-compatibility is not only achievable but is the natural outcome of a well-architected port.
+
+Recommended design for porting the Fantasy Map Generator to a headless system.
+
+### High-Level Architectural Vision
+
+The fundamental flaw of the current architecture for headless operation is the **tight coupling of generation logic and presentation (UI/rendering) logic**. Everything relies on the global `window` object, reads configuration from DOM elements, and directly manipulates SVG with libraries like `d3`.
+
+Our primary goal is to refactor this into two distinct, well-defined components:
+
+1. **The FMG Core Engine**: A pure, environment-agnostic JavaScript library. It will contain all the procedural generation logic. It will have no knowledge of browsers, DOM, or SVG. Its only job is to take a configuration object and produce a serializable data object representing the map. This engine will be the heart of both the headless system and the refactored web application.
+
+2. **The FMG Viewer/Client**: The existing web application, refactored to act as a client to the FMG Core Engine. It will handle user input, manage the SVG canvas, and call renderer modules to visualize the data produced by the engine.
+
+This separation is the key to achieving your cross-compatibility goal. The serialized data object (the `.map` file) becomes the universal "source of truth" that both systems can produce and consume.
+
+---
+
+### Phase 1: Designing the FMG Core Engine
+
+This is the most critical phase. The engine must be a collection of pure JavaScript modules that can run in any modern JS environment (Node.js, Deno, Bun, or a browser).
+
+#### 1.1. Input: The Configuration Object
+
+All functions that currently read from the DOM (`byId("statesNumber").value`, `manorsInput.valueAsNumber`, etc.) must be refactored. The main generation function of the engine will accept a single `config` object.
+
+**Example `config` object:**
+
+```javascript
+const config = {
+ seed: "123456789",
+ graph: {
+ width: 1920,
+ height: 1080,
+ points: 10000
+ },
+ generation: {
+ template: "continents",
+ cultures: 12,
+ culturesSet: "european",
+ states: 10,
+ provincesRatio: 40,
+ manors: 1000,
+ neutralRate: 1.2
+ },
+ display: {
+ populationRate: 10,
+ urbanization: 1,
+ // ... other options from the options panel
+ }
+};
+```
+
+#### 1.2. Output: The `MapData` Object
+
+The engine's primary function, let's call it `generateMap(config)`, will return a single, serializable object. This object will contain everything that is currently stored in global variables like `grid`, `pack`, `notes`, `options`, `seed`, etc.
+
+**Example `MapData` structure:**
+
+```javascript
+{
+ meta: {
+ version: "1.9", // FMG version for compatibility
+ generated: new Date().toISOString()
+ },
+ seed: "123456789",
+ config: { /* the config object used for generation */ },
+ grid: { /* grid data */ },
+ pack: { /* pack data */ },
+ notes: [ /* notes array */ ],
+ // ... any other top-level state
+}
+```
+
+#### 1.3. Module Refactoring Strategy
+
+Each of your provided `.js` files will be converted into an ES module within the engine.
+
+* **Remove IIFE and `window`:** Replace `window.MyModule = (function () { ... })();` with standard `export` statements.
+* **Dependency Injection & Pure Functions:** Modules should not rely on or modify a global `pack` or `grid` object. Instead, they should receive the current state of the map data as an argument and return the new data they've generated.
+
+**Example Refactoring: `biomes.js`**
+
+**Before (biomes.js):**
+
+```javascript
+"use strict";
+window.Biomes = (function () {
+ // ...
+ function define() {
+ // ... reads from global `pack` and `grid`
+ pack.cells.biome = new Uint8Array(pack.cells.i.length); // Direct modification
+ // ...
+ }
+ return {getDefault, define, getId};
+})();
+```
+
+**After (engine/modules/biomes.js):**
+
+```javascript
+"use strict";
+// No IIFE, no window dependency
+
+export function getDefaultBiomesData() {
+ // ... returns default biome data
+}
+
+export function defineBiomes(pack, grid) {
+ const {fl, r, h, c, g} = pack.cells;
+ const {temp, prec} = grid.cells;
+ const biome = new Uint8Array(pack.cells.i.length);
+
+ for (let cellId = 0; cellId < h.length; cellId++) {
+ // ... calculations ...
+ biome[cellId] = getId(/* ... */);
+ }
+
+ // Return only the data this module is responsible for
+ return { biome };
+}
+
+// ... other helper functions (getId, etc.)
+```
+
+#### 1.4. The Orchestrator (`engine/main.js`)
+
+A new "main" module in the engine will orchestrate the entire generation process, calling the refactored modules in the correct sequence and composing the final `MapData` object.
+
+```javascript
+// engine/main.js
+import { generateGrid } from './grid.js';
+import * as Heightmap from './heightmap.js';
+import * as Biomes from './biomes.js';
+// ... import all other engine modules
+
+export function generateMap(config) {
+ const seed = setSeed(config.seed);
+
+ let grid = generateGrid(config.graph);
+ grid.cells.h = Heightmap.generate(grid, config.generation.template);
+
+ // ... other initial grid steps (features, temperatures, etc.)
+
+ let pack = reGraph(grid); // Assume reGraph is refactored
+
+ // Sequentially build the pack object
+ const { biome } = Biomes.defineBiomes(pack, grid);
+ pack.cells.biome = biome;
+
+ const { cultures, cellsCulture } = Cultures.generate(pack, grid, config);
+ pack.cultures = cultures;
+ pack.cells.culture = cellsCulture;
+
+ // ... continue for states, burgs, rivers, etc.
+
+ return { meta, seed, config, grid, pack, notes: [] };
+}
+```
+
+---
+
+### Phase 2: Refactoring the Web Application (Viewer)
+
+The web app becomes a consumer of the Core Engine.
+
+#### 2.1. Separating Renderers
+
+All modules with rendering logic (`coa-renderer.js`, `ocean-layers.js`, parts of `routes-generator.js`, `markers-generator.js`) must be moved out of the engine and into a `viewer/renderers/` directory.
+
+* `COArenderer.draw` is already well-designed for this. It takes a `coa` object and renders it. Perfect.
+* The `Markers.generate` function should be split. The logic for *selecting candidate cells* (`listVolcanoes`, etc.) goes into the engine. The logic for *drawing the marker icon* (`addMarker` which creates SVG elements) goes into a `viewer/renderers/markers.js` module.
+* All `d3.select` and direct SVG manipulation must live exclusively in the Viewer.
+
+#### 2.2. New Web App Workflow
+
+1. **UI Interaction:** The user changes options in the UI.
+2. **Build Config:** A function gathers all UI settings and builds the `config` object.
+3. **Call Engine:** It calls `FMG_Engine.generateMap(config)`. This happens entirely in memory, with no DOM updates.
+4. **Receive `MapData`:** It receives the complete `MapData` object.
+5. **Render:** It calls a main `renderMap(MapData)` function, which in turn calls all the specific renderers (`renderBiomes`, `renderStates`, `renderRoutes`, etc.) to draw the SVG.
+
+---
+
+### Phase 3: Building the Headless System
+
+This now becomes incredibly simple. The headless application is just a new entry point that uses the FMG Core Engine.
+
+#### 3.1. Node.js CLI Application
+
+A simple command-line tool.
+
+**`package.json` dependencies:**
+`"dependencies": { "d3-quadtree": "...", "d3-polygon": "...", "delaunator": "..." }`
+(Note the absence of browser-specific libraries).
+
+**`generate.js`:**
+
+```javascript
+import { generateMap } from './engine/main.js';
+import fs from 'fs';
+
+// Load config from a JSON file passed as an argument
+const configFile = process.argv[2];
+const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
+
+console.log(`Generating map with seed: ${config.seed}...`);
+const mapData = generateMap(config);
+
+// Save the output
+const outputFile = config.output || `map_${config.seed}.map`;
+fs.writeFileSync(outputFile, JSON.stringify(mapData));
+console.log(`Map data saved to ${outputFile}`);
+```
+
+You could run it like: `node generate.js my_config.json`
+
+#### 3.2. REST API Server
+
+Similarly, you could wrap the engine in a web server (e.g., using Express.js) to provide map generation as a service.
+
+---
+
+### Data Persistence and Cross-Compatibility
+
+The `.map` file is the lynchpin.
+
+* **Format:** It should be a **JSON serialization of the `MapData` object**. This is human-readable, universally compatible, and simple. For large maps, consider compressing it with Gzip (resulting in a `.map.gz` file), which is standard practice.
+* **Workflow:**
+ * **Web App Save:** `const mapFileContent = JSON.stringify(currentMapData);` -> User downloads this content as `my_world.map`.
+ * **Headless Generation:** The CLI tool saves the `JSON.stringify(mapData)` output.
+ * **Web App Load:** User uploads a `.map` file -> `const mapData = JSON.parse(fileContent);` -> `renderMap(mapData);`.
+
+Since both systems use the **exact same FMG Core Engine** to generate the data structure, and they both save/load this structure in the same format (JSON), they are **guaranteed to be cross-compatible**.
+
+### Proposed Project Structure
+
+```
+/fmg
+├── /src
+│ ├── /engine # FMG Core Engine (headless, browser-agnostic)
+│ │ ├── main.js # Orchestrator (generateMap function)
+│ │ ├── modules/ # Refactored modules (biomes.js, cultures.js, etc.)
+│ │ └── utils/ # Agnostic utilities (math, array, etc.)
+│ │
+│ ├── /viewer # Web Application (UI and rendering)
+│ │ ├── main.js # Main UI logic, event handlers, orchestrates rendering
+│ │ ├── renderers/ # SVG rendering modules (mapRenderer.js, coaRenderer.js)
+│ │ └── ui/ # UI components, dialogs, etc.
+│ │
+│ └── /headless # Headless Application
+│ ├── cli.js # Command-line interface entry point
+│ └── server.js # (Optional) REST API server entry point
+│
+├── /assets # SVGs for charges, icons, etc.
+├── index.html
+├── package.json
+└── ...
+```
+
+### Step-by-Step Roadmap
+
+1. **Setup & Scaffolding:** Create the new project structure. Set up a build process (like Vite or Webpack) to handle modules for the browser.
+2. **Isolate Utilities:** [x] *DONE* Move all environment-agnostic utility functions (`arrayUtils`, `colorUtils`, `probabilityUtils`, etc.) into `engine/utils`.
+3. **Create the `config` Object:** Define the structure of the `config` object and modify the web app to build this object from the UI instead of having modules read from the DOM directly.
+4. **Refactor Incrementally:**
+ * Start with the simplest, most self-contained modules (e.g., `biomes`, `names-generator`). Convert them to the new engine module pattern (take data, return new data).
+ * Create the `engine/main.js` orchestrator and have it call these first few refactored modules.
+ * Modify the `viewer/main.js` to call the new `generateMap` function and then manually merge the results back into the global `pack`/`grid` objects for now.
+ * Separate the corresponding renderer for the refactored module.
+5. **Iterate:** Continue this process for all modules, one by one. `BurgsAndStates` and `Routes` will be the most complex due to their interdependencies and mixed logic.
+6. **Build Headless Entry Point:** Once the engine is fully decoupled, create the `headless/cli.js` file. It should be trivial at this point.
+7. **Finalize Viewer:** Complete the refactoring of the web app to fully rely on the `MapData` object returned by the engine, calling a master `renderMap` function instead of many individual `draw*` functions.
+
+This phased approach ensures that you can test and validate at each step, maintaining a working application throughout the process while moving methodically toward the final, robust, and portable architecture.
\ No newline at end of file
diff --git a/procedural/index.html b/procedural/index.html
new file mode 100644
index 00000000..dd1409b2
--- /dev/null
+++ b/procedural/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+ Fantasy Map Generator (Vite)
+
+
+
+
+
+
+
+
+
+
+ Generate Map
+ New Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/procedural/main.js b/procedural/main.js
new file mode 100644
index 00000000..c2ba10e3
--- /dev/null
+++ b/procedural/main.js
@@ -0,0 +1,29 @@
+// main.js (Viewer Entry Point)
+import './style.css';
+import { generateMap } from './src/engine/main.js'; // Import from our future engine
+
+console.log("FMG Viewer Initialized!");
+
+// Example of how you will eventually use the engine
+document.addEventListener('DOMContentLoaded', () => {
+ const generateButton = document.getElementById('generateMapButton'); // Assuming you have a button with this ID
+
+ generateButton.addEventListener('click', () => {
+ console.log("Generating map...");
+
+ // 1. Build config from UI
+ const config = {
+ seed: document.getElementById('optionsSeed').value || '12345',
+ graph: { width: 1024, height: 768, points: 10000 },
+ // ... more config from UI
+ };
+
+ // 2. Call the engine
+ const mapData = generateMap(config);
+
+ console.log("Map data generated by engine:", mapData);
+
+ // 3. Render the map (this function will be built out later)
+ // renderMap(mapData);
+ });
+});
\ No newline at end of file
diff --git a/procedural/package-lock.json b/procedural/package-lock.json
new file mode 100644
index 00000000..aeeea19e
--- /dev/null
+++ b/procedural/package-lock.json
@@ -0,0 +1,1080 @@
+{
+ "name": "procedural",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "procedural",
+ "version": "0.0.0",
+ "devDependencies": {
+ "vite": "^7.0.4"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+ "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+ "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+ "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+ "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+ "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+ "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+ "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+ "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+ "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+ "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+ "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+ "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+ "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+ "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+ "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
+ "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+ "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+ "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+ "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.8",
+ "@esbuild/android-arm": "0.25.8",
+ "@esbuild/android-arm64": "0.25.8",
+ "@esbuild/android-x64": "0.25.8",
+ "@esbuild/darwin-arm64": "0.25.8",
+ "@esbuild/darwin-x64": "0.25.8",
+ "@esbuild/freebsd-arm64": "0.25.8",
+ "@esbuild/freebsd-x64": "0.25.8",
+ "@esbuild/linux-arm": "0.25.8",
+ "@esbuild/linux-arm64": "0.25.8",
+ "@esbuild/linux-ia32": "0.25.8",
+ "@esbuild/linux-loong64": "0.25.8",
+ "@esbuild/linux-mips64el": "0.25.8",
+ "@esbuild/linux-ppc64": "0.25.8",
+ "@esbuild/linux-riscv64": "0.25.8",
+ "@esbuild/linux-s390x": "0.25.8",
+ "@esbuild/linux-x64": "0.25.8",
+ "@esbuild/netbsd-arm64": "0.25.8",
+ "@esbuild/netbsd-x64": "0.25.8",
+ "@esbuild/openbsd-arm64": "0.25.8",
+ "@esbuild/openbsd-x64": "0.25.8",
+ "@esbuild/openharmony-arm64": "0.25.8",
+ "@esbuild/sunos-x64": "0.25.8",
+ "@esbuild/win32-arm64": "0.25.8",
+ "@esbuild/win32-ia32": "0.25.8",
+ "@esbuild/win32-x64": "0.25.8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/flatqueue": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-2.0.3.tgz",
+ "integrity": "sha512-RZCWZNkmxzUOh8jqEcEGZCycb3B8KAfpPwg3H//cURasunYxsg1eIvE+QDSjX+ZPHTIVfINfK1aLTrVKKO0i4g==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12.17.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.46.2",
+ "@rollup/rollup-android-arm64": "4.46.2",
+ "@rollup/rollup-darwin-arm64": "4.46.2",
+ "@rollup/rollup-darwin-x64": "4.46.2",
+ "@rollup/rollup-freebsd-arm64": "4.46.2",
+ "@rollup/rollup-freebsd-x64": "4.46.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.46.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.46.2",
+ "@rollup/rollup-linux-arm64-musl": "4.46.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.46.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-musl": "4.46.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.46.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.46.2",
+ "@rollup/rollup-win32-x64-msvc": "4.46.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
+ "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.6",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.40.0",
+ "tinyglobby": "^0.2.14"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/procedural/package.json b/procedural/package.json
new file mode 100644
index 00000000..31da2e9b
--- /dev/null
+++ b/procedural/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "procedural",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^7.0.4"
+ }
+}
diff --git a/procedural/public/assets/charges/agnusDei.svg b/procedural/public/assets/charges/agnusDei.svg
new file mode 100644
index 00000000..60ea9d87
--- /dev/null
+++ b/procedural/public/assets/charges/agnusDei.svg
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/anchor.svg b/procedural/public/assets/charges/anchor.svg
new file mode 100644
index 00000000..59678314
--- /dev/null
+++ b/procedural/public/assets/charges/anchor.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/angel.svg b/procedural/public/assets/charges/angel.svg
new file mode 100644
index 00000000..e6f3ba3e
--- /dev/null
+++ b/procedural/public/assets/charges/angel.svg
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/annulet.svg b/procedural/public/assets/charges/annulet.svg
new file mode 100644
index 00000000..b3dad13d
--- /dev/null
+++ b/procedural/public/assets/charges/annulet.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/anvil.svg b/procedural/public/assets/charges/anvil.svg
new file mode 100644
index 00000000..2b6b0868
--- /dev/null
+++ b/procedural/public/assets/charges/anvil.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/apple.svg b/procedural/public/assets/charges/apple.svg
new file mode 100644
index 00000000..6679428d
--- /dev/null
+++ b/procedural/public/assets/charges/apple.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/arbalest.svg b/procedural/public/assets/charges/arbalest.svg
new file mode 100644
index 00000000..38adac50
--- /dev/null
+++ b/procedural/public/assets/charges/arbalest.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/arbalest2.svg b/procedural/public/assets/charges/arbalest2.svg
new file mode 100644
index 00000000..be9b1a6a
--- /dev/null
+++ b/procedural/public/assets/charges/arbalest2.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/archer.svg b/procedural/public/assets/charges/archer.svg
new file mode 100644
index 00000000..00891c1b
--- /dev/null
+++ b/procedural/public/assets/charges/archer.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/armEmbowedHoldingSabre.svg b/procedural/public/assets/charges/armEmbowedHoldingSabre.svg
new file mode 100644
index 00000000..2fbae9f6
--- /dev/null
+++ b/procedural/public/assets/charges/armEmbowedHoldingSabre.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/armEmbowedVambraced.svg b/procedural/public/assets/charges/armEmbowedVambraced.svg
new file mode 100644
index 00000000..ac1422b5
--- /dev/null
+++ b/procedural/public/assets/charges/armEmbowedVambraced.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/armEmbowedVambracedHoldingSword.svg b/procedural/public/assets/charges/armEmbowedVambracedHoldingSword.svg
new file mode 100644
index 00000000..d00d8bab
--- /dev/null
+++ b/procedural/public/assets/charges/armEmbowedVambracedHoldingSword.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/armillarySphere.svg b/procedural/public/assets/charges/armillarySphere.svg
new file mode 100644
index 00000000..45d09b8d
--- /dev/null
+++ b/procedural/public/assets/charges/armillarySphere.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/arrow.svg b/procedural/public/assets/charges/arrow.svg
new file mode 100644
index 00000000..2135169f
--- /dev/null
+++ b/procedural/public/assets/charges/arrow.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/arrowsSheaf.svg b/procedural/public/assets/charges/arrowsSheaf.svg
new file mode 100644
index 00000000..4b879e10
--- /dev/null
+++ b/procedural/public/assets/charges/arrowsSheaf.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/attire.svg b/procedural/public/assets/charges/attire.svg
new file mode 100644
index 00000000..99427a60
--- /dev/null
+++ b/procedural/public/assets/charges/attire.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/axe.svg b/procedural/public/assets/charges/axe.svg
new file mode 100644
index 00000000..e1608145
--- /dev/null
+++ b/procedural/public/assets/charges/axe.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/badgerStatant.svg b/procedural/public/assets/charges/badgerStatant.svg
new file mode 100644
index 00000000..29cd5f39
--- /dev/null
+++ b/procedural/public/assets/charges/badgerStatant.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/banner.svg b/procedural/public/assets/charges/banner.svg
new file mode 100644
index 00000000..8ca47b97
--- /dev/null
+++ b/procedural/public/assets/charges/banner.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/basilisk.svg b/procedural/public/assets/charges/basilisk.svg
new file mode 100644
index 00000000..b2755dff
--- /dev/null
+++ b/procedural/public/assets/charges/basilisk.svg
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bearPassant.svg b/procedural/public/assets/charges/bearPassant.svg
new file mode 100644
index 00000000..847ea7bd
--- /dev/null
+++ b/procedural/public/assets/charges/bearPassant.svg
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bearRampant.svg b/procedural/public/assets/charges/bearRampant.svg
new file mode 100644
index 00000000..418d6fb3
--- /dev/null
+++ b/procedural/public/assets/charges/bearRampant.svg
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bee.svg b/procedural/public/assets/charges/bee.svg
new file mode 100644
index 00000000..7f3a0069
--- /dev/null
+++ b/procedural/public/assets/charges/bee.svg
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bell.svg b/procedural/public/assets/charges/bell.svg
new file mode 100644
index 00000000..307b6493
--- /dev/null
+++ b/procedural/public/assets/charges/bell.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/billet.svg b/procedural/public/assets/charges/billet.svg
new file mode 100644
index 00000000..9482f5a7
--- /dev/null
+++ b/procedural/public/assets/charges/billet.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/boarHeadErased.svg b/procedural/public/assets/charges/boarHeadErased.svg
new file mode 100644
index 00000000..08348586
--- /dev/null
+++ b/procedural/public/assets/charges/boarHeadErased.svg
@@ -0,0 +1,44 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/boarRampant.svg b/procedural/public/assets/charges/boarRampant.svg
new file mode 100644
index 00000000..436fb439
--- /dev/null
+++ b/procedural/public/assets/charges/boarRampant.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/boat.svg b/procedural/public/assets/charges/boat.svg
new file mode 100644
index 00000000..b50f6194
--- /dev/null
+++ b/procedural/public/assets/charges/boat.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/boat2.svg b/procedural/public/assets/charges/boat2.svg
new file mode 100644
index 00000000..f3e37a5b
--- /dev/null
+++ b/procedural/public/assets/charges/boat2.svg
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bone.svg b/procedural/public/assets/charges/bone.svg
new file mode 100644
index 00000000..27a9e410
--- /dev/null
+++ b/procedural/public/assets/charges/bone.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bookClosed.svg b/procedural/public/assets/charges/bookClosed.svg
new file mode 100644
index 00000000..0cd74341
--- /dev/null
+++ b/procedural/public/assets/charges/bookClosed.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bookClosed2.svg b/procedural/public/assets/charges/bookClosed2.svg
new file mode 100644
index 00000000..bf8d2519
--- /dev/null
+++ b/procedural/public/assets/charges/bookClosed2.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bookOpen.svg b/procedural/public/assets/charges/bookOpen.svg
new file mode 100644
index 00000000..62f1c6fc
--- /dev/null
+++ b/procedural/public/assets/charges/bookOpen.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bow.svg b/procedural/public/assets/charges/bow.svg
new file mode 100644
index 00000000..b1f2c8ec
--- /dev/null
+++ b/procedural/public/assets/charges/bow.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bowWithArrow.svg b/procedural/public/assets/charges/bowWithArrow.svg
new file mode 100644
index 00000000..12d7f9ff
--- /dev/null
+++ b/procedural/public/assets/charges/bowWithArrow.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bowWithThreeArrows.svg b/procedural/public/assets/charges/bowWithThreeArrows.svg
new file mode 100644
index 00000000..0efc30f2
--- /dev/null
+++ b/procedural/public/assets/charges/bowWithThreeArrows.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bridge.svg b/procedural/public/assets/charges/bridge.svg
new file mode 100644
index 00000000..f5ce588e
--- /dev/null
+++ b/procedural/public/assets/charges/bridge.svg
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bridge2.svg b/procedural/public/assets/charges/bridge2.svg
new file mode 100644
index 00000000..6d3dfab5
--- /dev/null
+++ b/procedural/public/assets/charges/bridge2.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bucket.svg b/procedural/public/assets/charges/bucket.svg
new file mode 100644
index 00000000..56c7e83c
--- /dev/null
+++ b/procedural/public/assets/charges/bucket.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/buckle.svg b/procedural/public/assets/charges/buckle.svg
new file mode 100644
index 00000000..b83ba97d
--- /dev/null
+++ b/procedural/public/assets/charges/buckle.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bugleHorn.svg b/procedural/public/assets/charges/bugleHorn.svg
new file mode 100644
index 00000000..a7985ca6
--- /dev/null
+++ b/procedural/public/assets/charges/bugleHorn.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bugleHorn2.svg b/procedural/public/assets/charges/bugleHorn2.svg
new file mode 100644
index 00000000..cf5bcaa7
--- /dev/null
+++ b/procedural/public/assets/charges/bugleHorn2.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bullHeadCaboshed.svg b/procedural/public/assets/charges/bullHeadCaboshed.svg
new file mode 100644
index 00000000..d3ddeccc
--- /dev/null
+++ b/procedural/public/assets/charges/bullHeadCaboshed.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/bullPassant.svg b/procedural/public/assets/charges/bullPassant.svg
new file mode 100644
index 00000000..0314b64e
--- /dev/null
+++ b/procedural/public/assets/charges/bullPassant.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/butterfly.svg b/procedural/public/assets/charges/butterfly.svg
new file mode 100644
index 00000000..2c301fcf
--- /dev/null
+++ b/procedural/public/assets/charges/butterfly.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/camel.svg b/procedural/public/assets/charges/camel.svg
new file mode 100644
index 00000000..e2dd8bb9
--- /dev/null
+++ b/procedural/public/assets/charges/camel.svg
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cancer.svg b/procedural/public/assets/charges/cancer.svg
new file mode 100644
index 00000000..a8bf102c
--- /dev/null
+++ b/procedural/public/assets/charges/cancer.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cannon.svg b/procedural/public/assets/charges/cannon.svg
new file mode 100644
index 00000000..05e88b25
--- /dev/null
+++ b/procedural/public/assets/charges/cannon.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/caravel.svg b/procedural/public/assets/charges/caravel.svg
new file mode 100644
index 00000000..9eb57671
--- /dev/null
+++ b/procedural/public/assets/charges/caravel.svg
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/carreau.svg b/procedural/public/assets/charges/carreau.svg
new file mode 100644
index 00000000..bfeeb049
--- /dev/null
+++ b/procedural/public/assets/charges/carreau.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/castle.svg b/procedural/public/assets/charges/castle.svg
new file mode 100644
index 00000000..43a2fa38
--- /dev/null
+++ b/procedural/public/assets/charges/castle.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/castle2.svg b/procedural/public/assets/charges/castle2.svg
new file mode 100644
index 00000000..5f12a8aa
--- /dev/null
+++ b/procedural/public/assets/charges/castle2.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/catPassantGuardant.svg b/procedural/public/assets/charges/catPassantGuardant.svg
new file mode 100644
index 00000000..b49dc820
--- /dev/null
+++ b/procedural/public/assets/charges/catPassantGuardant.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cavalier.svg b/procedural/public/assets/charges/cavalier.svg
new file mode 100644
index 00000000..7bfad7ac
--- /dev/null
+++ b/procedural/public/assets/charges/cavalier.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/centaur.svg b/procedural/public/assets/charges/centaur.svg
new file mode 100644
index 00000000..f4ddeb22
--- /dev/null
+++ b/procedural/public/assets/charges/centaur.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/chain.svg b/procedural/public/assets/charges/chain.svg
new file mode 100644
index 00000000..cfe4a3bb
--- /dev/null
+++ b/procedural/public/assets/charges/chain.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/chalice.svg b/procedural/public/assets/charges/chalice.svg
new file mode 100644
index 00000000..0f4f95f6
--- /dev/null
+++ b/procedural/public/assets/charges/chalice.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cinquefoil.svg b/procedural/public/assets/charges/cinquefoil.svg
new file mode 100644
index 00000000..49db293e
--- /dev/null
+++ b/procedural/public/assets/charges/cinquefoil.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cock.svg b/procedural/public/assets/charges/cock.svg
new file mode 100644
index 00000000..4b100034
--- /dev/null
+++ b/procedural/public/assets/charges/cock.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/column.svg b/procedural/public/assets/charges/column.svg
new file mode 100644
index 00000000..38d8b2a0
--- /dev/null
+++ b/procedural/public/assets/charges/column.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/comet.svg b/procedural/public/assets/charges/comet.svg
new file mode 100644
index 00000000..096ddb67
--- /dev/null
+++ b/procedural/public/assets/charges/comet.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/compassRose.svg b/procedural/public/assets/charges/compassRose.svg
new file mode 100644
index 00000000..dec495cc
--- /dev/null
+++ b/procedural/public/assets/charges/compassRose.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cossack.svg b/procedural/public/assets/charges/cossack.svg
new file mode 100644
index 00000000..f5cf75ed
--- /dev/null
+++ b/procedural/public/assets/charges/cossack.svg
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cowHorns.svg b/procedural/public/assets/charges/cowHorns.svg
new file mode 100644
index 00000000..74378210
--- /dev/null
+++ b/procedural/public/assets/charges/cowHorns.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/cowStatant.svg b/procedural/public/assets/charges/cowStatant.svg
new file mode 100644
index 00000000..66b46823
--- /dev/null
+++ b/procedural/public/assets/charges/cowStatant.svg
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crescent.svg b/procedural/public/assets/charges/crescent.svg
new file mode 100644
index 00000000..9181995b
--- /dev/null
+++ b/procedural/public/assets/charges/crescent.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crocodile.svg b/procedural/public/assets/charges/crocodile.svg
new file mode 100644
index 00000000..442e456d
--- /dev/null
+++ b/procedural/public/assets/charges/crocodile.svg
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crosier.svg b/procedural/public/assets/charges/crosier.svg
new file mode 100644
index 00000000..631ef998
--- /dev/null
+++ b/procedural/public/assets/charges/crosier.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossAnkh.svg b/procedural/public/assets/charges/crossAnkh.svg
new file mode 100644
index 00000000..6bbac70e
--- /dev/null
+++ b/procedural/public/assets/charges/crossAnkh.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossArrowed.svg b/procedural/public/assets/charges/crossArrowed.svg
new file mode 100644
index 00000000..ff4b6c02
--- /dev/null
+++ b/procedural/public/assets/charges/crossArrowed.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossAvellane.svg b/procedural/public/assets/charges/crossAvellane.svg
new file mode 100644
index 00000000..303e7f72
--- /dev/null
+++ b/procedural/public/assets/charges/crossAvellane.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossBiparted.svg b/procedural/public/assets/charges/crossBiparted.svg
new file mode 100644
index 00000000..0e6ac5f8
--- /dev/null
+++ b/procedural/public/assets/charges/crossBiparted.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossBottony.svg b/procedural/public/assets/charges/crossBottony.svg
new file mode 100644
index 00000000..2d246b29
--- /dev/null
+++ b/procedural/public/assets/charges/crossBottony.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossBurgundy.svg b/procedural/public/assets/charges/crossBurgundy.svg
new file mode 100644
index 00000000..cb681714
--- /dev/null
+++ b/procedural/public/assets/charges/crossBurgundy.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossCalvary.svg b/procedural/public/assets/charges/crossCalvary.svg
new file mode 100644
index 00000000..dd0447b5
--- /dev/null
+++ b/procedural/public/assets/charges/crossCalvary.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossCarolingian.svg b/procedural/public/assets/charges/crossCarolingian.svg
new file mode 100644
index 00000000..761464a7
--- /dev/null
+++ b/procedural/public/assets/charges/crossCarolingian.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossCeltic.svg b/procedural/public/assets/charges/crossCeltic.svg
new file mode 100644
index 00000000..6abe10fe
--- /dev/null
+++ b/procedural/public/assets/charges/crossCeltic.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossCeltic2.svg b/procedural/public/assets/charges/crossCeltic2.svg
new file mode 100644
index 00000000..84628911
--- /dev/null
+++ b/procedural/public/assets/charges/crossCeltic2.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossCercelee.svg b/procedural/public/assets/charges/crossCercelee.svg
new file mode 100644
index 00000000..2b8bff16
--- /dev/null
+++ b/procedural/public/assets/charges/crossCercelee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossClechy.svg b/procedural/public/assets/charges/crossClechy.svg
new file mode 100644
index 00000000..c246534e
--- /dev/null
+++ b/procedural/public/assets/charges/crossClechy.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossDouble.svg b/procedural/public/assets/charges/crossDouble.svg
new file mode 100644
index 00000000..1a0e4bc8
--- /dev/null
+++ b/procedural/public/assets/charges/crossDouble.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossErminee.svg b/procedural/public/assets/charges/crossErminee.svg
new file mode 100644
index 00000000..1def3830
--- /dev/null
+++ b/procedural/public/assets/charges/crossErminee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossFitchy.svg b/procedural/public/assets/charges/crossFitchy.svg
new file mode 100644
index 00000000..954b8e70
--- /dev/null
+++ b/procedural/public/assets/charges/crossFitchy.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossFleury.svg b/procedural/public/assets/charges/crossFleury.svg
new file mode 100644
index 00000000..d617a15d
--- /dev/null
+++ b/procedural/public/assets/charges/crossFleury.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossFormee.svg b/procedural/public/assets/charges/crossFormee.svg
new file mode 100644
index 00000000..efef99d4
--- /dev/null
+++ b/procedural/public/assets/charges/crossFormee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossFormee2.svg b/procedural/public/assets/charges/crossFormee2.svg
new file mode 100644
index 00000000..2309e494
--- /dev/null
+++ b/procedural/public/assets/charges/crossFormee2.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossFourchy.svg b/procedural/public/assets/charges/crossFourchy.svg
new file mode 100644
index 00000000..9308a143
--- /dev/null
+++ b/procedural/public/assets/charges/crossFourchy.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossGamma.svg b/procedural/public/assets/charges/crossGamma.svg
new file mode 100644
index 00000000..d2da6490
--- /dev/null
+++ b/procedural/public/assets/charges/crossGamma.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossHummetty.svg b/procedural/public/assets/charges/crossHummetty.svg
new file mode 100644
index 00000000..e2676ab2
--- /dev/null
+++ b/procedural/public/assets/charges/crossHummetty.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossJerusalem.svg b/procedural/public/assets/charges/crossJerusalem.svg
new file mode 100644
index 00000000..54ba95dc
--- /dev/null
+++ b/procedural/public/assets/charges/crossJerusalem.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossLatin.svg b/procedural/public/assets/charges/crossLatin.svg
new file mode 100644
index 00000000..16f60138
--- /dev/null
+++ b/procedural/public/assets/charges/crossLatin.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossMaltese.svg b/procedural/public/assets/charges/crossMaltese.svg
new file mode 100644
index 00000000..5718dacf
--- /dev/null
+++ b/procedural/public/assets/charges/crossMaltese.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossMoline.svg b/procedural/public/assets/charges/crossMoline.svg
new file mode 100644
index 00000000..97f3b918
--- /dev/null
+++ b/procedural/public/assets/charges/crossMoline.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossOccitan.svg b/procedural/public/assets/charges/crossOccitan.svg
new file mode 100644
index 00000000..15fe7587
--- /dev/null
+++ b/procedural/public/assets/charges/crossOccitan.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossOrthodox.svg b/procedural/public/assets/charges/crossOrthodox.svg
new file mode 100644
index 00000000..9519f84d
--- /dev/null
+++ b/procedural/public/assets/charges/crossOrthodox.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPatonce.svg b/procedural/public/assets/charges/crossPatonce.svg
new file mode 100644
index 00000000..aaaec339
--- /dev/null
+++ b/procedural/public/assets/charges/crossPatonce.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPatriarchal.svg b/procedural/public/assets/charges/crossPatriarchal.svg
new file mode 100644
index 00000000..12338bc2
--- /dev/null
+++ b/procedural/public/assets/charges/crossPatriarchal.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPattee.svg b/procedural/public/assets/charges/crossPattee.svg
new file mode 100644
index 00000000..1c3bf761
--- /dev/null
+++ b/procedural/public/assets/charges/crossPattee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPatteeAlisee.svg b/procedural/public/assets/charges/crossPatteeAlisee.svg
new file mode 100644
index 00000000..801e7113
--- /dev/null
+++ b/procedural/public/assets/charges/crossPatteeAlisee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPommy.svg b/procedural/public/assets/charges/crossPommy.svg
new file mode 100644
index 00000000..da002347
--- /dev/null
+++ b/procedural/public/assets/charges/crossPommy.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossPotent.svg b/procedural/public/assets/charges/crossPotent.svg
new file mode 100644
index 00000000..cc3bb92f
--- /dev/null
+++ b/procedural/public/assets/charges/crossPotent.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossSaltire.svg b/procedural/public/assets/charges/crossSaltire.svg
new file mode 100644
index 00000000..8798ba45
--- /dev/null
+++ b/procedural/public/assets/charges/crossSaltire.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossSantiago.svg b/procedural/public/assets/charges/crossSantiago.svg
new file mode 100644
index 00000000..7e510a90
--- /dev/null
+++ b/procedural/public/assets/charges/crossSantiago.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crossTau.svg b/procedural/public/assets/charges/crossTau.svg
new file mode 100644
index 00000000..0789faf5
--- /dev/null
+++ b/procedural/public/assets/charges/crossTau.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossTemplar.svg b/procedural/public/assets/charges/crossTemplar.svg
new file mode 100644
index 00000000..9c33613c
--- /dev/null
+++ b/procedural/public/assets/charges/crossTemplar.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossTriquetra.svg b/procedural/public/assets/charges/crossTriquetra.svg
new file mode 100644
index 00000000..024225c1
--- /dev/null
+++ b/procedural/public/assets/charges/crossTriquetra.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossVoided.svg b/procedural/public/assets/charges/crossVoided.svg
new file mode 100644
index 00000000..0fa95bd7
--- /dev/null
+++ b/procedural/public/assets/charges/crossVoided.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crossedBones.svg b/procedural/public/assets/charges/crossedBones.svg
new file mode 100644
index 00000000..3b06442f
--- /dev/null
+++ b/procedural/public/assets/charges/crossedBones.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crosslet.svg b/procedural/public/assets/charges/crosslet.svg
new file mode 100644
index 00000000..645b68a3
--- /dev/null
+++ b/procedural/public/assets/charges/crosslet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/crown.svg b/procedural/public/assets/charges/crown.svg
new file mode 100644
index 00000000..00dbbacf
--- /dev/null
+++ b/procedural/public/assets/charges/crown.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/crown2.svg b/procedural/public/assets/charges/crown2.svg
new file mode 100644
index 00000000..f06a106e
--- /dev/null
+++ b/procedural/public/assets/charges/crown2.svg
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/deerHeadCaboshed.svg b/procedural/public/assets/charges/deerHeadCaboshed.svg
new file mode 100644
index 00000000..aca87f99
--- /dev/null
+++ b/procedural/public/assets/charges/deerHeadCaboshed.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/delf.svg b/procedural/public/assets/charges/delf.svg
new file mode 100644
index 00000000..227bd04f
--- /dev/null
+++ b/procedural/public/assets/charges/delf.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/dolphin.svg b/procedural/public/assets/charges/dolphin.svg
new file mode 100644
index 00000000..be5b00bd
--- /dev/null
+++ b/procedural/public/assets/charges/dolphin.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/donkeyHeadCaboshed.svg b/procedural/public/assets/charges/donkeyHeadCaboshed.svg
new file mode 100644
index 00000000..da5aee0a
--- /dev/null
+++ b/procedural/public/assets/charges/donkeyHeadCaboshed.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/dove.svg b/procedural/public/assets/charges/dove.svg
new file mode 100644
index 00000000..7406a6ec
--- /dev/null
+++ b/procedural/public/assets/charges/dove.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/doveDisplayed.svg b/procedural/public/assets/charges/doveDisplayed.svg
new file mode 100644
index 00000000..35bd14d6
--- /dev/null
+++ b/procedural/public/assets/charges/doveDisplayed.svg
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/dragonPassant.svg b/procedural/public/assets/charges/dragonPassant.svg
new file mode 100644
index 00000000..b8fbc58e
--- /dev/null
+++ b/procedural/public/assets/charges/dragonPassant.svg
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/dragonRampant.svg b/procedural/public/assets/charges/dragonRampant.svg
new file mode 100644
index 00000000..6ff64c2a
--- /dev/null
+++ b/procedural/public/assets/charges/dragonRampant.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/dragonfly.svg b/procedural/public/assets/charges/dragonfly.svg
new file mode 100644
index 00000000..c8b501ca
--- /dev/null
+++ b/procedural/public/assets/charges/dragonfly.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/drakkar.svg b/procedural/public/assets/charges/drakkar.svg
new file mode 100644
index 00000000..f36c3c4d
--- /dev/null
+++ b/procedural/public/assets/charges/drakkar.svg
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/drawingCompass.svg b/procedural/public/assets/charges/drawingCompass.svg
new file mode 100644
index 00000000..65a252d3
--- /dev/null
+++ b/procedural/public/assets/charges/drawingCompass.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/drum.svg b/procedural/public/assets/charges/drum.svg
new file mode 100644
index 00000000..bd8fd638
--- /dev/null
+++ b/procedural/public/assets/charges/drum.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/duck.svg b/procedural/public/assets/charges/duck.svg
new file mode 100644
index 00000000..940c3e4d
--- /dev/null
+++ b/procedural/public/assets/charges/duck.svg
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/eagle.svg b/procedural/public/assets/charges/eagle.svg
new file mode 100644
index 00000000..c52fd495
--- /dev/null
+++ b/procedural/public/assets/charges/eagle.svg
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/eagleTwoHeads.svg b/procedural/public/assets/charges/eagleTwoHeads.svg
new file mode 100644
index 00000000..cc8ac124
--- /dev/null
+++ b/procedural/public/assets/charges/eagleTwoHeads.svg
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/earOfWheat.svg b/procedural/public/assets/charges/earOfWheat.svg
new file mode 100644
index 00000000..a1b3072c
--- /dev/null
+++ b/procedural/public/assets/charges/earOfWheat.svg
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/elephant.svg b/procedural/public/assets/charges/elephant.svg
new file mode 100644
index 00000000..fed4c13f
--- /dev/null
+++ b/procedural/public/assets/charges/elephant.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/elephantHeadErased.svg b/procedural/public/assets/charges/elephantHeadErased.svg
new file mode 100644
index 00000000..f521413b
--- /dev/null
+++ b/procedural/public/assets/charges/elephantHeadErased.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/escallop.svg b/procedural/public/assets/charges/escallop.svg
new file mode 100644
index 00000000..4fda9dda
--- /dev/null
+++ b/procedural/public/assets/charges/escallop.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/estoile.svg b/procedural/public/assets/charges/estoile.svg
new file mode 100644
index 00000000..6966ccbc
--- /dev/null
+++ b/procedural/public/assets/charges/estoile.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/falchion.svg b/procedural/public/assets/charges/falchion.svg
new file mode 100644
index 00000000..ccb71d3d
--- /dev/null
+++ b/procedural/public/assets/charges/falchion.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/falcon.svg b/procedural/public/assets/charges/falcon.svg
new file mode 100644
index 00000000..5e4cebd5
--- /dev/null
+++ b/procedural/public/assets/charges/falcon.svg
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/fan.svg b/procedural/public/assets/charges/fan.svg
new file mode 100644
index 00000000..d7504ac6
--- /dev/null
+++ b/procedural/public/assets/charges/fan.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/fasces.svg b/procedural/public/assets/charges/fasces.svg
new file mode 100644
index 00000000..71e27285
--- /dev/null
+++ b/procedural/public/assets/charges/fasces.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/feather.svg b/procedural/public/assets/charges/feather.svg
new file mode 100644
index 00000000..0be55bf5
--- /dev/null
+++ b/procedural/public/assets/charges/feather.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/flamberge.svg b/procedural/public/assets/charges/flamberge.svg
new file mode 100644
index 00000000..ab9d2277
--- /dev/null
+++ b/procedural/public/assets/charges/flamberge.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/flangedMace.svg b/procedural/public/assets/charges/flangedMace.svg
new file mode 100644
index 00000000..901d942f
--- /dev/null
+++ b/procedural/public/assets/charges/flangedMace.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/fleurDeLis.svg b/procedural/public/assets/charges/fleurDeLis.svg
new file mode 100644
index 00000000..2583e48f
--- /dev/null
+++ b/procedural/public/assets/charges/fleurDeLis.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/fly.svg b/procedural/public/assets/charges/fly.svg
new file mode 100644
index 00000000..9c880f72
--- /dev/null
+++ b/procedural/public/assets/charges/fly.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/foot.svg b/procedural/public/assets/charges/foot.svg
new file mode 100644
index 00000000..22963e5b
--- /dev/null
+++ b/procedural/public/assets/charges/foot.svg
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/fountain.svg b/procedural/public/assets/charges/fountain.svg
new file mode 100644
index 00000000..1ed8ac67
--- /dev/null
+++ b/procedural/public/assets/charges/fountain.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/frog.svg b/procedural/public/assets/charges/frog.svg
new file mode 100644
index 00000000..392d3d05
--- /dev/null
+++ b/procedural/public/assets/charges/frog.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/fusil.svg b/procedural/public/assets/charges/fusil.svg
new file mode 100644
index 00000000..eac10ed4
--- /dev/null
+++ b/procedural/public/assets/charges/fusil.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/garb.svg b/procedural/public/assets/charges/garb.svg
new file mode 100644
index 00000000..fbefacd8
--- /dev/null
+++ b/procedural/public/assets/charges/garb.svg
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/gauntlet.svg b/procedural/public/assets/charges/gauntlet.svg
new file mode 100644
index 00000000..f00a03eb
--- /dev/null
+++ b/procedural/public/assets/charges/gauntlet.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/gear.svg b/procedural/public/assets/charges/gear.svg
new file mode 100644
index 00000000..2d128dfb
--- /dev/null
+++ b/procedural/public/assets/charges/gear.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/goat.svg b/procedural/public/assets/charges/goat.svg
new file mode 100644
index 00000000..721c3f15
--- /dev/null
+++ b/procedural/public/assets/charges/goat.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/goutte.svg b/procedural/public/assets/charges/goutte.svg
new file mode 100644
index 00000000..40f23183
--- /dev/null
+++ b/procedural/public/assets/charges/goutte.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/grapeBunch.svg b/procedural/public/assets/charges/grapeBunch.svg
new file mode 100644
index 00000000..38d4693f
--- /dev/null
+++ b/procedural/public/assets/charges/grapeBunch.svg
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/grapeBunch2.svg b/procedural/public/assets/charges/grapeBunch2.svg
new file mode 100644
index 00000000..0af7b6a6
--- /dev/null
+++ b/procedural/public/assets/charges/grapeBunch2.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/grenade.svg b/procedural/public/assets/charges/grenade.svg
new file mode 100644
index 00000000..07436784
--- /dev/null
+++ b/procedural/public/assets/charges/grenade.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/greyhoundCourant.svg b/procedural/public/assets/charges/greyhoundCourant.svg
new file mode 100644
index 00000000..916d70cf
--- /dev/null
+++ b/procedural/public/assets/charges/greyhoundCourant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/greyhoundRampant.svg b/procedural/public/assets/charges/greyhoundRampant.svg
new file mode 100644
index 00000000..cf607df5
--- /dev/null
+++ b/procedural/public/assets/charges/greyhoundRampant.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/greyhoundSejant.svg b/procedural/public/assets/charges/greyhoundSejant.svg
new file mode 100644
index 00000000..09efa81c
--- /dev/null
+++ b/procedural/public/assets/charges/greyhoundSejant.svg
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/griffinPassant.svg b/procedural/public/assets/charges/griffinPassant.svg
new file mode 100644
index 00000000..871b3591
--- /dev/null
+++ b/procedural/public/assets/charges/griffinPassant.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/griffinRampant.svg b/procedural/public/assets/charges/griffinRampant.svg
new file mode 100644
index 00000000..da2cfebe
--- /dev/null
+++ b/procedural/public/assets/charges/griffinRampant.svg
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/hand.svg b/procedural/public/assets/charges/hand.svg
new file mode 100644
index 00000000..91c279ae
--- /dev/null
+++ b/procedural/public/assets/charges/hand.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/harp.svg b/procedural/public/assets/charges/harp.svg
new file mode 100644
index 00000000..91e5ee08
--- /dev/null
+++ b/procedural/public/assets/charges/harp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/hatchet.svg b/procedural/public/assets/charges/hatchet.svg
new file mode 100644
index 00000000..37052556
--- /dev/null
+++ b/procedural/public/assets/charges/hatchet.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/head.svg b/procedural/public/assets/charges/head.svg
new file mode 100644
index 00000000..08e36060
--- /dev/null
+++ b/procedural/public/assets/charges/head.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/headWreathed.svg b/procedural/public/assets/charges/headWreathed.svg
new file mode 100644
index 00000000..b6dafc64
--- /dev/null
+++ b/procedural/public/assets/charges/headWreathed.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/heart.svg b/procedural/public/assets/charges/heart.svg
new file mode 100644
index 00000000..56d0940c
--- /dev/null
+++ b/procedural/public/assets/charges/heart.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/hedgehog.svg b/procedural/public/assets/charges/hedgehog.svg
new file mode 100644
index 00000000..c7a84caa
--- /dev/null
+++ b/procedural/public/assets/charges/hedgehog.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/helmet.svg b/procedural/public/assets/charges/helmet.svg
new file mode 100644
index 00000000..9099c7c3
--- /dev/null
+++ b/procedural/public/assets/charges/helmet.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/helmetCorinthian.svg b/procedural/public/assets/charges/helmetCorinthian.svg
new file mode 100644
index 00000000..3ccd9cdd
--- /dev/null
+++ b/procedural/public/assets/charges/helmetCorinthian.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/helmetGreat.svg b/procedural/public/assets/charges/helmetGreat.svg
new file mode 100644
index 00000000..b7a7bf49
--- /dev/null
+++ b/procedural/public/assets/charges/helmetGreat.svg
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/helmetZischagge.svg b/procedural/public/assets/charges/helmetZischagge.svg
new file mode 100644
index 00000000..8985d197
--- /dev/null
+++ b/procedural/public/assets/charges/helmetZischagge.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/heron.svg b/procedural/public/assets/charges/heron.svg
new file mode 100644
index 00000000..4893b082
--- /dev/null
+++ b/procedural/public/assets/charges/heron.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/hindStatant.svg b/procedural/public/assets/charges/hindStatant.svg
new file mode 100644
index 00000000..3f9cc429
--- /dev/null
+++ b/procedural/public/assets/charges/hindStatant.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/hook.svg b/procedural/public/assets/charges/hook.svg
new file mode 100644
index 00000000..d5679f64
--- /dev/null
+++ b/procedural/public/assets/charges/hook.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/horseHeadCouped.svg b/procedural/public/assets/charges/horseHeadCouped.svg
new file mode 100644
index 00000000..8c455c23
--- /dev/null
+++ b/procedural/public/assets/charges/horseHeadCouped.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/horsePassant.svg b/procedural/public/assets/charges/horsePassant.svg
new file mode 100644
index 00000000..b2361b84
--- /dev/null
+++ b/procedural/public/assets/charges/horsePassant.svg
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/horseRampant.svg b/procedural/public/assets/charges/horseRampant.svg
new file mode 100644
index 00000000..be7fee6c
--- /dev/null
+++ b/procedural/public/assets/charges/horseRampant.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/horseSalient.svg b/procedural/public/assets/charges/horseSalient.svg
new file mode 100644
index 00000000..21b5db48
--- /dev/null
+++ b/procedural/public/assets/charges/horseSalient.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/horseshoe.svg b/procedural/public/assets/charges/horseshoe.svg
new file mode 100644
index 00000000..3bd51f7a
--- /dev/null
+++ b/procedural/public/assets/charges/horseshoe.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/hourglass.svg b/procedural/public/assets/charges/hourglass.svg
new file mode 100644
index 00000000..87e7930b
--- /dev/null
+++ b/procedural/public/assets/charges/hourglass.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/key.svg b/procedural/public/assets/charges/key.svg
new file mode 100644
index 00000000..6fb883ea
--- /dev/null
+++ b/procedural/public/assets/charges/key.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ladder.svg b/procedural/public/assets/charges/ladder.svg
new file mode 100644
index 00000000..fc72f130
--- /dev/null
+++ b/procedural/public/assets/charges/ladder.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ladder2.svg b/procedural/public/assets/charges/ladder2.svg
new file mode 100644
index 00000000..5596e045
--- /dev/null
+++ b/procedural/public/assets/charges/ladder2.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ladybird.svg b/procedural/public/assets/charges/ladybird.svg
new file mode 100644
index 00000000..3cc1587e
--- /dev/null
+++ b/procedural/public/assets/charges/ladybird.svg
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lamb.svg b/procedural/public/assets/charges/lamb.svg
new file mode 100644
index 00000000..1d574b53
--- /dev/null
+++ b/procedural/public/assets/charges/lamb.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lambPassantReguardant.svg b/procedural/public/assets/charges/lambPassantReguardant.svg
new file mode 100644
index 00000000..90884eb7
--- /dev/null
+++ b/procedural/public/assets/charges/lambPassantReguardant.svg
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lanceHead.svg b/procedural/public/assets/charges/lanceHead.svg
new file mode 100644
index 00000000..255492db
--- /dev/null
+++ b/procedural/public/assets/charges/lanceHead.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lanceWithBanner.svg b/procedural/public/assets/charges/lanceWithBanner.svg
new file mode 100644
index 00000000..556e5ade
--- /dev/null
+++ b/procedural/public/assets/charges/lanceWithBanner.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/laurelWreath.svg b/procedural/public/assets/charges/laurelWreath.svg
new file mode 100644
index 00000000..989e00c4
--- /dev/null
+++ b/procedural/public/assets/charges/laurelWreath.svg
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/laurelWreath2.svg b/procedural/public/assets/charges/laurelWreath2.svg
new file mode 100644
index 00000000..6dfb373e
--- /dev/null
+++ b/procedural/public/assets/charges/laurelWreath2.svg
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lighthouse.svg b/procedural/public/assets/charges/lighthouse.svg
new file mode 100644
index 00000000..b66938f5
--- /dev/null
+++ b/procedural/public/assets/charges/lighthouse.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lionHeadCaboshed.svg b/procedural/public/assets/charges/lionHeadCaboshed.svg
new file mode 100644
index 00000000..63022fc0
--- /dev/null
+++ b/procedural/public/assets/charges/lionHeadCaboshed.svg
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lionHeadErased.svg b/procedural/public/assets/charges/lionHeadErased.svg
new file mode 100644
index 00000000..7ed39c67
--- /dev/null
+++ b/procedural/public/assets/charges/lionHeadErased.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/lionPassant.svg b/procedural/public/assets/charges/lionPassant.svg
new file mode 100644
index 00000000..7dd6b396
--- /dev/null
+++ b/procedural/public/assets/charges/lionPassant.svg
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lionPassantGuardant.svg b/procedural/public/assets/charges/lionPassantGuardant.svg
new file mode 100644
index 00000000..584e8835
--- /dev/null
+++ b/procedural/public/assets/charges/lionPassantGuardant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/lionRampant.svg b/procedural/public/assets/charges/lionRampant.svg
new file mode 100644
index 00000000..8aab2b09
--- /dev/null
+++ b/procedural/public/assets/charges/lionRampant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/lionSejant.svg b/procedural/public/assets/charges/lionSejant.svg
new file mode 100644
index 00000000..23ca98db
--- /dev/null
+++ b/procedural/public/assets/charges/lionSejant.svg
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lizard.svg b/procedural/public/assets/charges/lizard.svg
new file mode 100644
index 00000000..dc8dbf9d
--- /dev/null
+++ b/procedural/public/assets/charges/lizard.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lochaberAxe.svg b/procedural/public/assets/charges/lochaberAxe.svg
new file mode 100644
index 00000000..cf09c61e
--- /dev/null
+++ b/procedural/public/assets/charges/lochaberAxe.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/log.svg b/procedural/public/assets/charges/log.svg
new file mode 100644
index 00000000..387fad44
--- /dev/null
+++ b/procedural/public/assets/charges/log.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lozenge.svg b/procedural/public/assets/charges/lozenge.svg
new file mode 100644
index 00000000..4cfb22c9
--- /dev/null
+++ b/procedural/public/assets/charges/lozenge.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lozengeFaceted.svg b/procedural/public/assets/charges/lozengeFaceted.svg
new file mode 100644
index 00000000..63a575e3
--- /dev/null
+++ b/procedural/public/assets/charges/lozengeFaceted.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lozengePloye.svg b/procedural/public/assets/charges/lozengePloye.svg
new file mode 100644
index 00000000..0187c681
--- /dev/null
+++ b/procedural/public/assets/charges/lozengePloye.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lute.svg b/procedural/public/assets/charges/lute.svg
new file mode 100644
index 00000000..c88cc397
--- /dev/null
+++ b/procedural/public/assets/charges/lute.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/lymphad.svg b/procedural/public/assets/charges/lymphad.svg
new file mode 100644
index 00000000..457f10d7
--- /dev/null
+++ b/procedural/public/assets/charges/lymphad.svg
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/lyre.svg b/procedural/public/assets/charges/lyre.svg
new file mode 100644
index 00000000..d89b8550
--- /dev/null
+++ b/procedural/public/assets/charges/lyre.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mace.svg b/procedural/public/assets/charges/mace.svg
new file mode 100644
index 00000000..a3f0e074
--- /dev/null
+++ b/procedural/public/assets/charges/mace.svg
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/maces.svg b/procedural/public/assets/charges/maces.svg
new file mode 100644
index 00000000..93988b80
--- /dev/null
+++ b/procedural/public/assets/charges/maces.svg
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mallet.svg b/procedural/public/assets/charges/mallet.svg
new file mode 100644
index 00000000..ecb418f9
--- /dev/null
+++ b/procedural/public/assets/charges/mallet.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mantle.svg b/procedural/public/assets/charges/mantle.svg
new file mode 100644
index 00000000..5493c30b
--- /dev/null
+++ b/procedural/public/assets/charges/mantle.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mapleLeaf.svg b/procedural/public/assets/charges/mapleLeaf.svg
new file mode 100644
index 00000000..93e63767
--- /dev/null
+++ b/procedural/public/assets/charges/mapleLeaf.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/martenCourant.svg b/procedural/public/assets/charges/martenCourant.svg
new file mode 100644
index 00000000..1760d038
--- /dev/null
+++ b/procedural/public/assets/charges/martenCourant.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mascle.svg b/procedural/public/assets/charges/mascle.svg
new file mode 100644
index 00000000..b867e47a
--- /dev/null
+++ b/procedural/public/assets/charges/mascle.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mastiffStatant.svg b/procedural/public/assets/charges/mastiffStatant.svg
new file mode 100644
index 00000000..14feb266
--- /dev/null
+++ b/procedural/public/assets/charges/mastiffStatant.svg
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/millstone.svg b/procedural/public/assets/charges/millstone.svg
new file mode 100644
index 00000000..f8d523ca
--- /dev/null
+++ b/procedural/public/assets/charges/millstone.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mitre.svg b/procedural/public/assets/charges/mitre.svg
new file mode 100644
index 00000000..1a20e002
--- /dev/null
+++ b/procedural/public/assets/charges/mitre.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/monk.svg b/procedural/public/assets/charges/monk.svg
new file mode 100644
index 00000000..8ede0359
--- /dev/null
+++ b/procedural/public/assets/charges/monk.svg
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/moonInCrescent.svg b/procedural/public/assets/charges/moonInCrescent.svg
new file mode 100644
index 00000000..27fdb512
--- /dev/null
+++ b/procedural/public/assets/charges/moonInCrescent.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet.svg b/procedural/public/assets/charges/mullet.svg
new file mode 100644
index 00000000..2658e971
--- /dev/null
+++ b/procedural/public/assets/charges/mullet.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet10.svg b/procedural/public/assets/charges/mullet10.svg
new file mode 100644
index 00000000..60ed608a
--- /dev/null
+++ b/procedural/public/assets/charges/mullet10.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet4.svg b/procedural/public/assets/charges/mullet4.svg
new file mode 100644
index 00000000..37242f99
--- /dev/null
+++ b/procedural/public/assets/charges/mullet4.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet6.svg b/procedural/public/assets/charges/mullet6.svg
new file mode 100644
index 00000000..1a1972aa
--- /dev/null
+++ b/procedural/public/assets/charges/mullet6.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet6Faceted.svg b/procedural/public/assets/charges/mullet6Faceted.svg
new file mode 100644
index 00000000..1ca0c335
--- /dev/null
+++ b/procedural/public/assets/charges/mullet6Faceted.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet6Pierced.svg b/procedural/public/assets/charges/mullet6Pierced.svg
new file mode 100644
index 00000000..96d6f279
--- /dev/null
+++ b/procedural/public/assets/charges/mullet6Pierced.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet7.svg b/procedural/public/assets/charges/mullet7.svg
new file mode 100644
index 00000000..53321978
--- /dev/null
+++ b/procedural/public/assets/charges/mullet7.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mullet8.svg b/procedural/public/assets/charges/mullet8.svg
new file mode 100644
index 00000000..0239a883
--- /dev/null
+++ b/procedural/public/assets/charges/mullet8.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mulletFaceted.svg b/procedural/public/assets/charges/mulletFaceted.svg
new file mode 100644
index 00000000..a43fe26e
--- /dev/null
+++ b/procedural/public/assets/charges/mulletFaceted.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/mulletPierced.svg b/procedural/public/assets/charges/mulletPierced.svg
new file mode 100644
index 00000000..b469bf0c
--- /dev/null
+++ b/procedural/public/assets/charges/mulletPierced.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/oak.svg b/procedural/public/assets/charges/oak.svg
new file mode 100644
index 00000000..b01da9b4
--- /dev/null
+++ b/procedural/public/assets/charges/oak.svg
@@ -0,0 +1,347 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/orb.svg b/procedural/public/assets/charges/orb.svg
new file mode 100644
index 00000000..2d7354f5
--- /dev/null
+++ b/procedural/public/assets/charges/orb.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ouroboros.svg b/procedural/public/assets/charges/ouroboros.svg
new file mode 100644
index 00000000..f0f39146
--- /dev/null
+++ b/procedural/public/assets/charges/ouroboros.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/owl.svg b/procedural/public/assets/charges/owl.svg
new file mode 100644
index 00000000..015a7f81
--- /dev/null
+++ b/procedural/public/assets/charges/owl.svg
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/owlDisplayed.svg b/procedural/public/assets/charges/owlDisplayed.svg
new file mode 100644
index 00000000..353fe5e7
--- /dev/null
+++ b/procedural/public/assets/charges/owlDisplayed.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/palace.svg b/procedural/public/assets/charges/palace.svg
new file mode 100644
index 00000000..9e03dd4b
--- /dev/null
+++ b/procedural/public/assets/charges/palace.svg
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/palmTree.svg b/procedural/public/assets/charges/palmTree.svg
new file mode 100644
index 00000000..590d14a3
--- /dev/null
+++ b/procedural/public/assets/charges/palmTree.svg
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/parrot.svg b/procedural/public/assets/charges/parrot.svg
new file mode 100644
index 00000000..92d7dd10
--- /dev/null
+++ b/procedural/public/assets/charges/parrot.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/peacock.svg b/procedural/public/assets/charges/peacock.svg
new file mode 100644
index 00000000..064831f4
--- /dev/null
+++ b/procedural/public/assets/charges/peacock.svg
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/peacockInPride.svg b/procedural/public/assets/charges/peacockInPride.svg
new file mode 100644
index 00000000..b8b4f8c7
--- /dev/null
+++ b/procedural/public/assets/charges/peacockInPride.svg
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pear.svg b/procedural/public/assets/charges/pear.svg
new file mode 100644
index 00000000..d421e208
--- /dev/null
+++ b/procedural/public/assets/charges/pear.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pegasus.svg b/procedural/public/assets/charges/pegasus.svg
new file mode 100644
index 00000000..5335022d
--- /dev/null
+++ b/procedural/public/assets/charges/pegasus.svg
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pike.svg b/procedural/public/assets/charges/pike.svg
new file mode 100644
index 00000000..812002b7
--- /dev/null
+++ b/procedural/public/assets/charges/pike.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pillar.svg b/procedural/public/assets/charges/pillar.svg
new file mode 100644
index 00000000..d9aa943f
--- /dev/null
+++ b/procedural/public/assets/charges/pillar.svg
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pincers.svg b/procedural/public/assets/charges/pincers.svg
new file mode 100644
index 00000000..2e91f728
--- /dev/null
+++ b/procedural/public/assets/charges/pincers.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pineCone.svg b/procedural/public/assets/charges/pineCone.svg
new file mode 100644
index 00000000..c326f2a2
--- /dev/null
+++ b/procedural/public/assets/charges/pineCone.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pineTree.svg b/procedural/public/assets/charges/pineTree.svg
new file mode 100644
index 00000000..2abebe11
--- /dev/null
+++ b/procedural/public/assets/charges/pineTree.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pique.svg b/procedural/public/assets/charges/pique.svg
new file mode 100644
index 00000000..06c5836e
--- /dev/null
+++ b/procedural/public/assets/charges/pique.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/plaice.svg b/procedural/public/assets/charges/plaice.svg
new file mode 100644
index 00000000..8325d1ca
--- /dev/null
+++ b/procedural/public/assets/charges/plaice.svg
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/plough.svg b/procedural/public/assets/charges/plough.svg
new file mode 100644
index 00000000..7c1f42fc
--- /dev/null
+++ b/procedural/public/assets/charges/plough.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ploughshare.svg b/procedural/public/assets/charges/ploughshare.svg
new file mode 100644
index 00000000..45928f03
--- /dev/null
+++ b/procedural/public/assets/charges/ploughshare.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/porcupine.svg b/procedural/public/assets/charges/porcupine.svg
new file mode 100644
index 00000000..bdc53081
--- /dev/null
+++ b/procedural/public/assets/charges/porcupine.svg
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/portcullis.svg b/procedural/public/assets/charges/portcullis.svg
new file mode 100644
index 00000000..de154326
--- /dev/null
+++ b/procedural/public/assets/charges/portcullis.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/pot.svg b/procedural/public/assets/charges/pot.svg
new file mode 100644
index 00000000..64cc0ff6
--- /dev/null
+++ b/procedural/public/assets/charges/pot.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/quatrefoil.svg b/procedural/public/assets/charges/quatrefoil.svg
new file mode 100644
index 00000000..ff33f762
--- /dev/null
+++ b/procedural/public/assets/charges/quatrefoil.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/rabbitSejant.svg b/procedural/public/assets/charges/rabbitSejant.svg
new file mode 100644
index 00000000..c61e0a9b
--- /dev/null
+++ b/procedural/public/assets/charges/rabbitSejant.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/raft.svg b/procedural/public/assets/charges/raft.svg
new file mode 100644
index 00000000..d5b728b4
--- /dev/null
+++ b/procedural/public/assets/charges/raft.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/rake.svg b/procedural/public/assets/charges/rake.svg
new file mode 100644
index 00000000..6feed725
--- /dev/null
+++ b/procedural/public/assets/charges/rake.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ramHeadErased.svg b/procedural/public/assets/charges/ramHeadErased.svg
new file mode 100644
index 00000000..c971f7b4
--- /dev/null
+++ b/procedural/public/assets/charges/ramHeadErased.svg
@@ -0,0 +1,75 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/ramPassant.svg b/procedural/public/assets/charges/ramPassant.svg
new file mode 100644
index 00000000..421e416c
--- /dev/null
+++ b/procedural/public/assets/charges/ramPassant.svg
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ramsHorn.svg b/procedural/public/assets/charges/ramsHorn.svg
new file mode 100644
index 00000000..26585cc9
--- /dev/null
+++ b/procedural/public/assets/charges/ramsHorn.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/rapier.svg b/procedural/public/assets/charges/rapier.svg
new file mode 100644
index 00000000..fed83920
--- /dev/null
+++ b/procedural/public/assets/charges/rapier.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ratRampant.svg b/procedural/public/assets/charges/ratRampant.svg
new file mode 100644
index 00000000..13c6746a
--- /dev/null
+++ b/procedural/public/assets/charges/ratRampant.svg
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/raven.svg b/procedural/public/assets/charges/raven.svg
new file mode 100644
index 00000000..6e3c4eca
--- /dev/null
+++ b/procedural/public/assets/charges/raven.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/rhinoceros.svg b/procedural/public/assets/charges/rhinoceros.svg
new file mode 100644
index 00000000..1d43c4f4
--- /dev/null
+++ b/procedural/public/assets/charges/rhinoceros.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon1.svg b/procedural/public/assets/charges/ribbon1.svg
new file mode 100644
index 00000000..03bf3515
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon1.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon2.svg b/procedural/public/assets/charges/ribbon2.svg
new file mode 100644
index 00000000..0f15bb04
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon2.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon3.svg b/procedural/public/assets/charges/ribbon3.svg
new file mode 100644
index 00000000..dd168991
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon3.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon4.svg b/procedural/public/assets/charges/ribbon4.svg
new file mode 100644
index 00000000..bab35959
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon4.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon5.svg b/procedural/public/assets/charges/ribbon5.svg
new file mode 100644
index 00000000..3c718bce
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon5.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon6.svg b/procedural/public/assets/charges/ribbon6.svg
new file mode 100644
index 00000000..a32eede7
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon6.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon7.svg b/procedural/public/assets/charges/ribbon7.svg
new file mode 100644
index 00000000..2d9a1e21
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon7.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/ribbon8.svg b/procedural/public/assets/charges/ribbon8.svg
new file mode 100644
index 00000000..f7c672a7
--- /dev/null
+++ b/procedural/public/assets/charges/ribbon8.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/rose.svg b/procedural/public/assets/charges/rose.svg
new file mode 100644
index 00000000..7ce11665
--- /dev/null
+++ b/procedural/public/assets/charges/rose.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/roundel.svg b/procedural/public/assets/charges/roundel.svg
new file mode 100644
index 00000000..344991ed
--- /dev/null
+++ b/procedural/public/assets/charges/roundel.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/roundel2.svg b/procedural/public/assets/charges/roundel2.svg
new file mode 100644
index 00000000..fffa7ad4
--- /dev/null
+++ b/procedural/public/assets/charges/roundel2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/public/assets/charges/rustre.svg b/procedural/public/assets/charges/rustre.svg
new file mode 100644
index 00000000..60c253a1
--- /dev/null
+++ b/procedural/public/assets/charges/rustre.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sabre.svg b/procedural/public/assets/charges/sabre.svg
new file mode 100644
index 00000000..0f1f002d
--- /dev/null
+++ b/procedural/public/assets/charges/sabre.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sabre2.svg b/procedural/public/assets/charges/sabre2.svg
new file mode 100644
index 00000000..2466a761
--- /dev/null
+++ b/procedural/public/assets/charges/sabre2.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sabresCrossed.svg b/procedural/public/assets/charges/sabresCrossed.svg
new file mode 100644
index 00000000..e9cec5f4
--- /dev/null
+++ b/procedural/public/assets/charges/sabresCrossed.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sagittarius.svg b/procedural/public/assets/charges/sagittarius.svg
new file mode 100644
index 00000000..ac5eaeeb
--- /dev/null
+++ b/procedural/public/assets/charges/sagittarius.svg
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/salmon.svg b/procedural/public/assets/charges/salmon.svg
new file mode 100644
index 00000000..bda9bf49
--- /dev/null
+++ b/procedural/public/assets/charges/salmon.svg
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/saw.svg b/procedural/public/assets/charges/saw.svg
new file mode 100644
index 00000000..c356263f
--- /dev/null
+++ b/procedural/public/assets/charges/saw.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scale.svg b/procedural/public/assets/charges/scale.svg
new file mode 100644
index 00000000..b2da96ea
--- /dev/null
+++ b/procedural/public/assets/charges/scale.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scaleImbalanced.svg b/procedural/public/assets/charges/scaleImbalanced.svg
new file mode 100644
index 00000000..ea151649
--- /dev/null
+++ b/procedural/public/assets/charges/scaleImbalanced.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scalesHanging.svg b/procedural/public/assets/charges/scalesHanging.svg
new file mode 100644
index 00000000..1065211b
--- /dev/null
+++ b/procedural/public/assets/charges/scalesHanging.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sceptre.svg b/procedural/public/assets/charges/sceptre.svg
new file mode 100644
index 00000000..57869cbf
--- /dev/null
+++ b/procedural/public/assets/charges/sceptre.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scissors.svg b/procedural/public/assets/charges/scissors.svg
new file mode 100644
index 00000000..a7437f1e
--- /dev/null
+++ b/procedural/public/assets/charges/scissors.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scissors2.svg b/procedural/public/assets/charges/scissors2.svg
new file mode 100644
index 00000000..109d8238
--- /dev/null
+++ b/procedural/public/assets/charges/scissors2.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scorpion.svg b/procedural/public/assets/charges/scorpion.svg
new file mode 100644
index 00000000..c132511d
--- /dev/null
+++ b/procedural/public/assets/charges/scorpion.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scrollClosed.svg b/procedural/public/assets/charges/scrollClosed.svg
new file mode 100644
index 00000000..1f7d8034
--- /dev/null
+++ b/procedural/public/assets/charges/scrollClosed.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scythe.svg b/procedural/public/assets/charges/scythe.svg
new file mode 100644
index 00000000..b8d0a04d
--- /dev/null
+++ b/procedural/public/assets/charges/scythe.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/scythe2.svg b/procedural/public/assets/charges/scythe2.svg
new file mode 100644
index 00000000..7fa9de5d
--- /dev/null
+++ b/procedural/public/assets/charges/scythe2.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/serpent.svg b/procedural/public/assets/charges/serpent.svg
new file mode 100644
index 00000000..e12c9570
--- /dev/null
+++ b/procedural/public/assets/charges/serpent.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sextifoil.svg b/procedural/public/assets/charges/sextifoil.svg
new file mode 100644
index 00000000..ad6e3303
--- /dev/null
+++ b/procedural/public/assets/charges/sextifoil.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/shears.svg b/procedural/public/assets/charges/shears.svg
new file mode 100644
index 00000000..3b3daeb4
--- /dev/null
+++ b/procedural/public/assets/charges/shears.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/shield.svg b/procedural/public/assets/charges/shield.svg
new file mode 100644
index 00000000..89ad41de
--- /dev/null
+++ b/procedural/public/assets/charges/shield.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/shipWheel.svg b/procedural/public/assets/charges/shipWheel.svg
new file mode 100644
index 00000000..fa23136c
--- /dev/null
+++ b/procedural/public/assets/charges/shipWheel.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sickle.svg b/procedural/public/assets/charges/sickle.svg
new file mode 100644
index 00000000..753eeeaa
--- /dev/null
+++ b/procedural/public/assets/charges/sickle.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/skeleton.svg b/procedural/public/assets/charges/skeleton.svg
new file mode 100644
index 00000000..cde28dae
--- /dev/null
+++ b/procedural/public/assets/charges/skeleton.svg
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/skull.svg b/procedural/public/assets/charges/skull.svg
new file mode 100644
index 00000000..c7dc7044
--- /dev/null
+++ b/procedural/public/assets/charges/skull.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/skull2.svg b/procedural/public/assets/charges/skull2.svg
new file mode 100644
index 00000000..a580afd7
--- /dev/null
+++ b/procedural/public/assets/charges/skull2.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/snail.svg b/procedural/public/assets/charges/snail.svg
new file mode 100644
index 00000000..387dc48a
--- /dev/null
+++ b/procedural/public/assets/charges/snail.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/snake.svg b/procedural/public/assets/charges/snake.svg
new file mode 100644
index 00000000..532f2c0c
--- /dev/null
+++ b/procedural/public/assets/charges/snake.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/snowflake.svg b/procedural/public/assets/charges/snowflake.svg
new file mode 100644
index 00000000..d85d067b
--- /dev/null
+++ b/procedural/public/assets/charges/snowflake.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/spear.svg b/procedural/public/assets/charges/spear.svg
new file mode 100644
index 00000000..9944f9d5
--- /dev/null
+++ b/procedural/public/assets/charges/spear.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/spiral.svg b/procedural/public/assets/charges/spiral.svg
new file mode 100644
index 00000000..05736127
--- /dev/null
+++ b/procedural/public/assets/charges/spiral.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/squirrel.svg b/procedural/public/assets/charges/squirrel.svg
new file mode 100644
index 00000000..bf2cc68b
--- /dev/null
+++ b/procedural/public/assets/charges/squirrel.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/stagLodgedRegardant.svg b/procedural/public/assets/charges/stagLodgedRegardant.svg
new file mode 100644
index 00000000..024f648c
--- /dev/null
+++ b/procedural/public/assets/charges/stagLodgedRegardant.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/stagPassant.svg b/procedural/public/assets/charges/stagPassant.svg
new file mode 100644
index 00000000..745ed25e
--- /dev/null
+++ b/procedural/public/assets/charges/stagPassant.svg
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/stagsAttires.svg b/procedural/public/assets/charges/stagsAttires.svg
new file mode 100644
index 00000000..c8b3692b
--- /dev/null
+++ b/procedural/public/assets/charges/stagsAttires.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/stirrup.svg b/procedural/public/assets/charges/stirrup.svg
new file mode 100644
index 00000000..4350ad26
--- /dev/null
+++ b/procedural/public/assets/charges/stirrup.svg
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sun.svg b/procedural/public/assets/charges/sun.svg
new file mode 100644
index 00000000..6d58a55c
--- /dev/null
+++ b/procedural/public/assets/charges/sun.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sunInSplendour.svg b/procedural/public/assets/charges/sunInSplendour.svg
new file mode 100644
index 00000000..4de9c571
--- /dev/null
+++ b/procedural/public/assets/charges/sunInSplendour.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sunInSplendour2.svg b/procedural/public/assets/charges/sunInSplendour2.svg
new file mode 100644
index 00000000..d56c221d
--- /dev/null
+++ b/procedural/public/assets/charges/sunInSplendour2.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/swallow.svg b/procedural/public/assets/charges/swallow.svg
new file mode 100644
index 00000000..bf363a15
--- /dev/null
+++ b/procedural/public/assets/charges/swallow.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/swan.svg b/procedural/public/assets/charges/swan.svg
new file mode 100644
index 00000000..26a345f7
--- /dev/null
+++ b/procedural/public/assets/charges/swan.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/swanErased.svg b/procedural/public/assets/charges/swanErased.svg
new file mode 100644
index 00000000..510c380a
--- /dev/null
+++ b/procedural/public/assets/charges/swanErased.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/sword.svg b/procedural/public/assets/charges/sword.svg
new file mode 100644
index 00000000..cdf8853b
--- /dev/null
+++ b/procedural/public/assets/charges/sword.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/talbotPassant.svg b/procedural/public/assets/charges/talbotPassant.svg
new file mode 100644
index 00000000..121e6ba4
--- /dev/null
+++ b/procedural/public/assets/charges/talbotPassant.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/talbotSejant.svg b/procedural/public/assets/charges/talbotSejant.svg
new file mode 100644
index 00000000..e89d4b90
--- /dev/null
+++ b/procedural/public/assets/charges/talbotSejant.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/template.svg b/procedural/public/assets/charges/template.svg
new file mode 100644
index 00000000..cad51295
--- /dev/null
+++ b/procedural/public/assets/charges/template.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/thistle.svg b/procedural/public/assets/charges/thistle.svg
new file mode 100644
index 00000000..09cff52a
--- /dev/null
+++ b/procedural/public/assets/charges/thistle.svg
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/tower.svg b/procedural/public/assets/charges/tower.svg
new file mode 100644
index 00000000..9754803a
--- /dev/null
+++ b/procedural/public/assets/charges/tower.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/tree.svg b/procedural/public/assets/charges/tree.svg
new file mode 100644
index 00000000..0e2854fc
--- /dev/null
+++ b/procedural/public/assets/charges/tree.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/trefle.svg b/procedural/public/assets/charges/trefle.svg
new file mode 100644
index 00000000..8180526b
--- /dev/null
+++ b/procedural/public/assets/charges/trefle.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/trefoil.svg b/procedural/public/assets/charges/trefoil.svg
new file mode 100644
index 00000000..438c993e
--- /dev/null
+++ b/procedural/public/assets/charges/trefoil.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/triangle.svg b/procedural/public/assets/charges/triangle.svg
new file mode 100644
index 00000000..0a06d67c
--- /dev/null
+++ b/procedural/public/assets/charges/triangle.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/trianglePierced.svg b/procedural/public/assets/charges/trianglePierced.svg
new file mode 100644
index 00000000..6bbe2fc2
--- /dev/null
+++ b/procedural/public/assets/charges/trianglePierced.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/trowel.svg b/procedural/public/assets/charges/trowel.svg
new file mode 100644
index 00000000..b9533474
--- /dev/null
+++ b/procedural/public/assets/charges/trowel.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/unicornRampant.svg b/procedural/public/assets/charges/unicornRampant.svg
new file mode 100644
index 00000000..a3102dbc
--- /dev/null
+++ b/procedural/public/assets/charges/unicornRampant.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wasp.svg b/procedural/public/assets/charges/wasp.svg
new file mode 100644
index 00000000..9d54306c
--- /dev/null
+++ b/procedural/public/assets/charges/wasp.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wheatStalk.svg b/procedural/public/assets/charges/wheatStalk.svg
new file mode 100644
index 00000000..c6113ccd
--- /dev/null
+++ b/procedural/public/assets/charges/wheatStalk.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wheel.svg b/procedural/public/assets/charges/wheel.svg
new file mode 100644
index 00000000..6133eee9
--- /dev/null
+++ b/procedural/public/assets/charges/wheel.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/windmill.svg b/procedural/public/assets/charges/windmill.svg
new file mode 100644
index 00000000..b814ef1e
--- /dev/null
+++ b/procedural/public/assets/charges/windmill.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wing.svg b/procedural/public/assets/charges/wing.svg
new file mode 100644
index 00000000..d38622f2
--- /dev/null
+++ b/procedural/public/assets/charges/wing.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wingSword.svg b/procedural/public/assets/charges/wingSword.svg
new file mode 100644
index 00000000..75c2f80e
--- /dev/null
+++ b/procedural/public/assets/charges/wingSword.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wolfHeadErased.svg b/procedural/public/assets/charges/wolfHeadErased.svg
new file mode 100644
index 00000000..518ea83f
--- /dev/null
+++ b/procedural/public/assets/charges/wolfHeadErased.svg
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/procedural/public/assets/charges/wolfPassant.svg b/procedural/public/assets/charges/wolfPassant.svg
new file mode 100644
index 00000000..06f8761a
--- /dev/null
+++ b/procedural/public/assets/charges/wolfPassant.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wolfRampant.svg b/procedural/public/assets/charges/wolfRampant.svg
new file mode 100644
index 00000000..f5c7f57c
--- /dev/null
+++ b/procedural/public/assets/charges/wolfRampant.svg
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wolfStatant.svg b/procedural/public/assets/charges/wolfStatant.svg
new file mode 100644
index 00000000..d592b9ef
--- /dev/null
+++ b/procedural/public/assets/charges/wolfStatant.svg
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wyvern.svg b/procedural/public/assets/charges/wyvern.svg
new file mode 100644
index 00000000..f09b821e
--- /dev/null
+++ b/procedural/public/assets/charges/wyvern.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/charges/wyvernWithWingsDisplayed.svg b/procedural/public/assets/charges/wyvernWithWingsDisplayed.svg
new file mode 100644
index 00000000..d3aef9b4
--- /dev/null
+++ b/procedural/public/assets/charges/wyvernWithWingsDisplayed.svg
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/procedural/public/assets/heightmaps/africa-centric.png b/procedural/public/assets/heightmaps/africa-centric.png
new file mode 100644
index 00000000..02e4a311
Binary files /dev/null and b/procedural/public/assets/heightmaps/africa-centric.png differ
diff --git a/procedural/public/assets/heightmaps/arabia.png b/procedural/public/assets/heightmaps/arabia.png
new file mode 100644
index 00000000..27946711
Binary files /dev/null and b/procedural/public/assets/heightmaps/arabia.png differ
diff --git a/procedural/public/assets/heightmaps/atlantics.png b/procedural/public/assets/heightmaps/atlantics.png
new file mode 100644
index 00000000..be123705
Binary files /dev/null and b/procedural/public/assets/heightmaps/atlantics.png differ
diff --git a/procedural/public/assets/heightmaps/britain.png b/procedural/public/assets/heightmaps/britain.png
new file mode 100644
index 00000000..60e08da4
Binary files /dev/null and b/procedural/public/assets/heightmaps/britain.png differ
diff --git a/procedural/public/assets/heightmaps/caribbean.png b/procedural/public/assets/heightmaps/caribbean.png
new file mode 100644
index 00000000..e4a8ed05
Binary files /dev/null and b/procedural/public/assets/heightmaps/caribbean.png differ
diff --git a/procedural/public/assets/heightmaps/east-asia.png b/procedural/public/assets/heightmaps/east-asia.png
new file mode 100644
index 00000000..41144db5
Binary files /dev/null and b/procedural/public/assets/heightmaps/east-asia.png differ
diff --git a/procedural/public/assets/heightmaps/eurasia.png b/procedural/public/assets/heightmaps/eurasia.png
new file mode 100644
index 00000000..bdbdb4d7
Binary files /dev/null and b/procedural/public/assets/heightmaps/eurasia.png differ
diff --git a/procedural/public/assets/heightmaps/europe-accented.png b/procedural/public/assets/heightmaps/europe-accented.png
new file mode 100644
index 00000000..9be9480e
Binary files /dev/null and b/procedural/public/assets/heightmaps/europe-accented.png differ
diff --git a/procedural/public/assets/heightmaps/europe-and-central-asia.png b/procedural/public/assets/heightmaps/europe-and-central-asia.png
new file mode 100644
index 00000000..c23e97ed
Binary files /dev/null and b/procedural/public/assets/heightmaps/europe-and-central-asia.png differ
diff --git a/procedural/public/assets/heightmaps/europe-central.png b/procedural/public/assets/heightmaps/europe-central.png
new file mode 100644
index 00000000..b220f546
Binary files /dev/null and b/procedural/public/assets/heightmaps/europe-central.png differ
diff --git a/procedural/public/assets/heightmaps/europe-north.png b/procedural/public/assets/heightmaps/europe-north.png
new file mode 100644
index 00000000..1bb49184
Binary files /dev/null and b/procedural/public/assets/heightmaps/europe-north.png differ
diff --git a/procedural/public/assets/heightmaps/europe.png b/procedural/public/assets/heightmaps/europe.png
new file mode 100644
index 00000000..59dfdfea
Binary files /dev/null and b/procedural/public/assets/heightmaps/europe.png differ
diff --git a/procedural/public/assets/heightmaps/greenland.png b/procedural/public/assets/heightmaps/greenland.png
new file mode 100644
index 00000000..3136c539
Binary files /dev/null and b/procedural/public/assets/heightmaps/greenland.png differ
diff --git a/procedural/public/assets/heightmaps/hellenica.png b/procedural/public/assets/heightmaps/hellenica.png
new file mode 100644
index 00000000..2681d6ec
Binary files /dev/null and b/procedural/public/assets/heightmaps/hellenica.png differ
diff --git a/procedural/public/assets/heightmaps/iceland.png b/procedural/public/assets/heightmaps/iceland.png
new file mode 100644
index 00000000..88463158
Binary files /dev/null and b/procedural/public/assets/heightmaps/iceland.png differ
diff --git a/procedural/public/assets/heightmaps/import-rules.txt b/procedural/public/assets/heightmaps/import-rules.txt
new file mode 100644
index 00000000..69499114
--- /dev/null
+++ b/procedural/public/assets/heightmaps/import-rules.txt
@@ -0,0 +1,8 @@
+To get heightmap with correct height scale:
+1. Open https://tangrams.github.io/heightmapper
+2. Toggle off auto-exposure
+3. Set max elevation to 2000
+4. Set min elevation to -500
+5. Find region you like
+6. Render image
+7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used
diff --git a/procedural/public/assets/heightmaps/indian-ocean.png b/procedural/public/assets/heightmaps/indian-ocean.png
new file mode 100644
index 00000000..860ca952
Binary files /dev/null and b/procedural/public/assets/heightmaps/indian-ocean.png differ
diff --git a/procedural/public/assets/heightmaps/mediterranean-sea.png b/procedural/public/assets/heightmaps/mediterranean-sea.png
new file mode 100644
index 00000000..6a7c8bb3
Binary files /dev/null and b/procedural/public/assets/heightmaps/mediterranean-sea.png differ
diff --git a/procedural/public/assets/heightmaps/middle-east.png b/procedural/public/assets/heightmaps/middle-east.png
new file mode 100644
index 00000000..bfcc55bb
Binary files /dev/null and b/procedural/public/assets/heightmaps/middle-east.png differ
diff --git a/procedural/public/assets/heightmaps/north-america.png b/procedural/public/assets/heightmaps/north-america.png
new file mode 100644
index 00000000..1c1f1ad5
Binary files /dev/null and b/procedural/public/assets/heightmaps/north-america.png differ
diff --git a/procedural/public/assets/heightmaps/us-centric.png b/procedural/public/assets/heightmaps/us-centric.png
new file mode 100644
index 00000000..7094df6a
Binary files /dev/null and b/procedural/public/assets/heightmaps/us-centric.png differ
diff --git a/procedural/public/assets/heightmaps/us-mainland.png b/procedural/public/assets/heightmaps/us-mainland.png
new file mode 100644
index 00000000..3b1984e7
Binary files /dev/null and b/procedural/public/assets/heightmaps/us-mainland.png differ
diff --git a/procedural/public/assets/heightmaps/world-from-pacific.png b/procedural/public/assets/heightmaps/world-from-pacific.png
new file mode 100644
index 00000000..02043165
Binary files /dev/null and b/procedural/public/assets/heightmaps/world-from-pacific.png differ
diff --git a/procedural/public/assets/heightmaps/world.png b/procedural/public/assets/heightmaps/world.png
new file mode 100644
index 00000000..22a79298
Binary files /dev/null and b/procedural/public/assets/heightmaps/world.png differ
diff --git a/procedural/public/assets/images/Discord.png b/procedural/public/assets/images/Discord.png
new file mode 100644
index 00000000..78dab317
Binary files /dev/null and b/procedural/public/assets/images/Discord.png differ
diff --git a/procedural/public/assets/images/Facebook.png b/procedural/public/assets/images/Facebook.png
new file mode 100644
index 00000000..3d249fd9
Binary files /dev/null and b/procedural/public/assets/images/Facebook.png differ
diff --git a/procedural/public/assets/images/Pinterest.png b/procedural/public/assets/images/Pinterest.png
new file mode 100644
index 00000000..fcc85914
Binary files /dev/null and b/procedural/public/assets/images/Pinterest.png differ
diff --git a/procedural/public/assets/images/Reddit.png b/procedural/public/assets/images/Reddit.png
new file mode 100644
index 00000000..4637f3a4
Binary files /dev/null and b/procedural/public/assets/images/Reddit.png differ
diff --git a/procedural/public/assets/images/Twitter.png b/procedural/public/assets/images/Twitter.png
new file mode 100644
index 00000000..05e0c2c2
Binary files /dev/null and b/procedural/public/assets/images/Twitter.png differ
diff --git a/procedural/public/assets/images/icons/favicon-16x16.png b/procedural/public/assets/images/icons/favicon-16x16.png
new file mode 100644
index 00000000..ddd75b4a
Binary files /dev/null and b/procedural/public/assets/images/icons/favicon-16x16.png differ
diff --git a/procedural/public/assets/images/icons/favicon-32x32.png b/procedural/public/assets/images/icons/favicon-32x32.png
new file mode 100644
index 00000000..13e5179d
Binary files /dev/null and b/procedural/public/assets/images/icons/favicon-32x32.png differ
diff --git a/procedural/public/assets/images/icons/icon_x512.png b/procedural/public/assets/images/icons/icon_x512.png
new file mode 100644
index 00000000..f1f8c9aa
Binary files /dev/null and b/procedural/public/assets/images/icons/icon_x512.png differ
diff --git a/procedural/public/assets/images/icons/maskable_icon_x128.png b/procedural/public/assets/images/icons/maskable_icon_x128.png
new file mode 100644
index 00000000..fa877d1b
Binary files /dev/null and b/procedural/public/assets/images/icons/maskable_icon_x128.png differ
diff --git a/procedural/public/assets/images/icons/maskable_icon_x192.png b/procedural/public/assets/images/icons/maskable_icon_x192.png
new file mode 100644
index 00000000..3322eab3
Binary files /dev/null and b/procedural/public/assets/images/icons/maskable_icon_x192.png differ
diff --git a/procedural/public/assets/images/icons/maskable_icon_x384.png b/procedural/public/assets/images/icons/maskable_icon_x384.png
new file mode 100644
index 00000000..c7e7e705
Binary files /dev/null and b/procedural/public/assets/images/icons/maskable_icon_x384.png differ
diff --git a/procedural/public/assets/images/icons/maskable_icon_x512.png b/procedural/public/assets/images/icons/maskable_icon_x512.png
new file mode 100644
index 00000000..5b2361fd
Binary files /dev/null and b/procedural/public/assets/images/icons/maskable_icon_x512.png differ
diff --git a/procedural/public/assets/images/kiwiroo.png b/procedural/public/assets/images/kiwiroo.png
new file mode 100644
index 00000000..4f34ae7e
Binary files /dev/null and b/procedural/public/assets/images/kiwiroo.png differ
diff --git a/procedural/public/assets/images/pattern1.png b/procedural/public/assets/images/pattern1.png
new file mode 100644
index 00000000..59375796
Binary files /dev/null and b/procedural/public/assets/images/pattern1.png differ
diff --git a/procedural/public/assets/images/pattern2.png b/procedural/public/assets/images/pattern2.png
new file mode 100644
index 00000000..e96f68fa
Binary files /dev/null and b/procedural/public/assets/images/pattern2.png differ
diff --git a/procedural/public/assets/images/pattern3.png b/procedural/public/assets/images/pattern3.png
new file mode 100644
index 00000000..636fde6f
Binary files /dev/null and b/procedural/public/assets/images/pattern3.png differ
diff --git a/procedural/public/assets/images/pattern4.png b/procedural/public/assets/images/pattern4.png
new file mode 100644
index 00000000..d96aa18a
Binary files /dev/null and b/procedural/public/assets/images/pattern4.png differ
diff --git a/procedural/public/assets/images/pattern5.png b/procedural/public/assets/images/pattern5.png
new file mode 100644
index 00000000..82a2af7c
Binary files /dev/null and b/procedural/public/assets/images/pattern5.png differ
diff --git a/procedural/public/assets/images/pattern6.png b/procedural/public/assets/images/pattern6.png
new file mode 100644
index 00000000..dc9271ef
Binary files /dev/null and b/procedural/public/assets/images/pattern6.png differ
diff --git a/procedural/public/assets/images/preview.png b/procedural/public/assets/images/preview.png
new file mode 100644
index 00000000..2b150732
Binary files /dev/null and b/procedural/public/assets/images/preview.png differ
diff --git a/procedural/public/assets/images/textures/antique-big.jpg b/procedural/public/assets/images/textures/antique-big.jpg
new file mode 100644
index 00000000..711b1681
Binary files /dev/null and b/procedural/public/assets/images/textures/antique-big.jpg differ
diff --git a/procedural/public/assets/images/textures/antique-small.jpg b/procedural/public/assets/images/textures/antique-small.jpg
new file mode 100644
index 00000000..851b5d07
Binary files /dev/null and b/procedural/public/assets/images/textures/antique-small.jpg differ
diff --git a/procedural/public/assets/images/textures/folded-paper-big.jpg b/procedural/public/assets/images/textures/folded-paper-big.jpg
new file mode 100644
index 00000000..c2c4d761
Binary files /dev/null and b/procedural/public/assets/images/textures/folded-paper-big.jpg differ
diff --git a/procedural/public/assets/images/textures/folded-paper-small.jpg b/procedural/public/assets/images/textures/folded-paper-small.jpg
new file mode 100644
index 00000000..88418a13
Binary files /dev/null and b/procedural/public/assets/images/textures/folded-paper-small.jpg differ
diff --git a/procedural/public/assets/images/textures/gray-paper.jpg b/procedural/public/assets/images/textures/gray-paper.jpg
new file mode 100644
index 00000000..238d6e4c
Binary files /dev/null and b/procedural/public/assets/images/textures/gray-paper.jpg differ
diff --git a/procedural/public/assets/images/textures/iran-small.jpg b/procedural/public/assets/images/textures/iran-small.jpg
new file mode 100644
index 00000000..39f34512
Binary files /dev/null and b/procedural/public/assets/images/textures/iran-small.jpg differ
diff --git a/procedural/public/assets/images/textures/marble-big.jpg b/procedural/public/assets/images/textures/marble-big.jpg
new file mode 100644
index 00000000..c1d2a6d4
Binary files /dev/null and b/procedural/public/assets/images/textures/marble-big.jpg differ
diff --git a/procedural/public/assets/images/textures/marble-blue-big.jpg b/procedural/public/assets/images/textures/marble-blue-big.jpg
new file mode 100644
index 00000000..dbfc0975
Binary files /dev/null and b/procedural/public/assets/images/textures/marble-blue-big.jpg differ
diff --git a/procedural/public/assets/images/textures/marble-blue-small.jpg b/procedural/public/assets/images/textures/marble-blue-small.jpg
new file mode 100644
index 00000000..2e95fdcb
Binary files /dev/null and b/procedural/public/assets/images/textures/marble-blue-small.jpg differ
diff --git a/procedural/public/assets/images/textures/marble-small.jpg b/procedural/public/assets/images/textures/marble-small.jpg
new file mode 100644
index 00000000..10d1a9ab
Binary files /dev/null and b/procedural/public/assets/images/textures/marble-small.jpg differ
diff --git a/procedural/public/assets/images/textures/mars-big.jpg b/procedural/public/assets/images/textures/mars-big.jpg
new file mode 100644
index 00000000..3fd39dae
Binary files /dev/null and b/procedural/public/assets/images/textures/mars-big.jpg differ
diff --git a/procedural/public/assets/images/textures/mars-small.jpg b/procedural/public/assets/images/textures/mars-small.jpg
new file mode 100644
index 00000000..75de8dd3
Binary files /dev/null and b/procedural/public/assets/images/textures/mars-small.jpg differ
diff --git a/procedural/public/assets/images/textures/mauritania-small.jpg b/procedural/public/assets/images/textures/mauritania-small.jpg
new file mode 100644
index 00000000..22d9cecf
Binary files /dev/null and b/procedural/public/assets/images/textures/mauritania-small.jpg differ
diff --git a/procedural/public/assets/images/textures/mercury-big.jpg b/procedural/public/assets/images/textures/mercury-big.jpg
new file mode 100644
index 00000000..7e06f0ee
Binary files /dev/null and b/procedural/public/assets/images/textures/mercury-big.jpg differ
diff --git a/procedural/public/assets/images/textures/mercury-small.jpg b/procedural/public/assets/images/textures/mercury-small.jpg
new file mode 100644
index 00000000..53f31ee3
Binary files /dev/null and b/procedural/public/assets/images/textures/mercury-small.jpg differ
diff --git a/procedural/public/assets/images/textures/ocean.jpg b/procedural/public/assets/images/textures/ocean.jpg
new file mode 100644
index 00000000..981366ca
Binary files /dev/null and b/procedural/public/assets/images/textures/ocean.jpg differ
diff --git a/procedural/public/assets/images/textures/pergamena-small.jpg b/procedural/public/assets/images/textures/pergamena-small.jpg
new file mode 100644
index 00000000..951f9eda
Binary files /dev/null and b/procedural/public/assets/images/textures/pergamena-small.jpg differ
diff --git a/procedural/public/assets/images/textures/plaster.jpg b/procedural/public/assets/images/textures/plaster.jpg
new file mode 100644
index 00000000..8ec85c81
Binary files /dev/null and b/procedural/public/assets/images/textures/plaster.jpg differ
diff --git a/procedural/public/assets/images/textures/soiled-paper-vertical.png b/procedural/public/assets/images/textures/soiled-paper-vertical.png
new file mode 100644
index 00000000..f8bb720e
Binary files /dev/null and b/procedural/public/assets/images/textures/soiled-paper-vertical.png differ
diff --git a/procedural/public/assets/images/textures/soiled-paper.jpg b/procedural/public/assets/images/textures/soiled-paper.jpg
new file mode 100644
index 00000000..00333992
Binary files /dev/null and b/procedural/public/assets/images/textures/soiled-paper.jpg differ
diff --git a/procedural/public/assets/images/textures/spain-small.jpg b/procedural/public/assets/images/textures/spain-small.jpg
new file mode 100644
index 00000000..a413f508
Binary files /dev/null and b/procedural/public/assets/images/textures/spain-small.jpg differ
diff --git a/procedural/public/assets/images/textures/timbercut-big.jpg b/procedural/public/assets/images/textures/timbercut-big.jpg
new file mode 100644
index 00000000..7dc9b656
Binary files /dev/null and b/procedural/public/assets/images/textures/timbercut-big.jpg differ
diff --git a/procedural/public/assets/images/textures/timbercut-small.jpg b/procedural/public/assets/images/textures/timbercut-small.jpg
new file mode 100644
index 00000000..a73e47be
Binary files /dev/null and b/procedural/public/assets/images/textures/timbercut-small.jpg differ
diff --git a/procedural/public/vite.svg b/procedural/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/procedural/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/procedural/src/default_prompt.md b/procedural/src/default_prompt.md
new file mode 100644
index 00000000..29225109
--- /dev/null
+++ b/procedural/src/default_prompt.md
@@ -0,0 +1,84 @@
+# cultures-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `cultures-generator.js`.
+
+**File Content:**
+```javascript
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./cultures-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./cultures-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in cultures-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into cultures-generator_render.md
diff --git a/procedural/src/engine/main.js b/procedural/src/engine/main.js
new file mode 100644
index 00000000..53fb5ee6
--- /dev/null
+++ b/procedural/src/engine/main.js
@@ -0,0 +1,100 @@
+// src/engine/main.js
+
+// Import all the refactored engine modules
+import * as Biomes from "./modules/biomes.js";
+import * as BurgsAndStates from "./modules/burgs-and-states.js";
+import * as COA from "./modules/coa-generator.js";
+import * as COArenderer from "./modules/coa-renderer.js";
+import * as Cultures from "./modules/cultures-generator.js";
+import * as Features from "./modules/features.js";
+import * as Heightmap from "./modules/heightmap-generator.js";
+import * as Lakes from "./modules/lakes.js";
+import * as Markers from "./modules/markers-generator.js";
+import * as Military from "./modules/military-generator.js";
+import * as Names from "./modules/names-generator.js";
+import * as Provinces from "./modules/provinces-generator.js";
+import * as Religions from "./modules/religions-generator.js";
+import * as Rivers from "./modules/river-generator.js";
+import * as Routes from "./modules/routes-generator.js";
+import * as Zones from "./modules/zones-generator.js";
+import { Voronoi } from "./modules/voronoi.js";
+import * as Utils from "./utils/index.js";
+
+// Import the new utility modules
+import * as Graph from "./utils/graphUtils.js";
+import * as Geography from "./utils/geography.js";
+import * as Cell from "./utils/cell.js";
+
+/**
+ * The main entry point for the headless map generation engine.
+ * @param {object} config - A comprehensive configuration object.
+ * @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }.
+ */
+export function generate(config) {
+ const timeStart = performance.now();
+ const { TIME, WARN, INFO, ERROR } = Utils; // Core logging utils
+
+ // Set up PRNG
+ const seed = config.seed || Utils.generateSeed();
+ Math.random = Utils.aleaPRNG(seed);
+ INFO && console.group("Generating Map with Seed: " + seed);
+
+ // --- Grid Generation ---
+ let grid = Graph.generateGrid(config.graph);
+ grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils);
+ grid = Features.markupGrid(grid, config, Utils);
+ const { mapCoordinates } = Geography.defineMapSize(grid, config.map);
+ grid = Geography.addLakesInDeepDepressions(grid, config.lakes, Utils);
+ grid = Geography.openNearSeaLakes(grid, config.lakes, Utils);
+
+ // --- Core Data Calculation ---
+ const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils);
+ grid.cells.temp = temp;
+ const { prec } = Geography.generatePrecipitation(grid, mapCoordinates, config.precipitation, Utils);
+ grid.cells.prec = prec;
+
+ // --- Pack Generation ---
+ let pack = Graph.reGraph(grid, Utils);
+ pack = Features.markupPack(pack, config, Utils, { Lakes });
+
+ // --- River Generation ---
+ const riverGenerationResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names });
+ pack = riverGenerationResult.pack;
+
+ // --- Biome and Population ---
+ const { biome } = Biomes.define(pack, grid, config.biomes, Utils);
+ pack.cells.biome = biome;
+ const { s, pop } = Cell.rankCells(pack, grid, Utils, { biomesData: Biomes.getDefault() });
+ pack.cells.s = s;
+ pack.cells.pop = pop;
+
+ // --- Cultures, States, and Burgs ---
+ const culturesResult = Cultures.generate(pack, grid, config.cultures, Utils, { Names });
+ pack.cultures = culturesResult.cultures;
+ pack.cells.culture = culturesResult.culture;
+ Cultures.expand(pack, config.cultures, Utils, { biomesData: Biomes.getDefault() });
+
+ const burgsAndStatesResult = BurgsAndStates.generate(pack, grid, config.burgs, Utils, { Names, COA, getPolesOfInaccessibility: Utils.getPolesOfInaccessibility });
+ pack.burgs = burgsAndStatesResult.burgs;
+ pack.states = burgsAndStatesResult.states;
+ pack.cells.burg = burgsAndStatesResult.cells.burg;
+ pack.cells.state = burgsAndStatesResult.cells.state;
+
+ // --- Final Touches ---
+ // Routes.generate(pack, Utils);
+ // Religions.generate(pack, config.religions, Utils, { Names, BurgsAndStates });
+ // BurgsAndStates.defineStateForms(null, pack, Utils, { Names });
+ // Provinces.generate(pack, config.provinces, Utils, { BurgsAndStates, Names, COA });
+ // BurgsAndStates.defineBurgFeatures(null, pack, Utils);
+ // Rivers.specify(pack, Utils, { Names });
+ // Features.specify(pack, grid, Utils, { Lakes });
+ // Military.generate(pack, config.military, Utils, { Names });
+ // Markers.generate(pack, config.markers, Utils);
+ // Zones.generate(pack, config.zones, Utils);
+
+ WARN && console.warn(`TOTAL GENERATION TIME: ${Utils.rn((performance.now() - timeStart) / 1000, 2)}s`);
+ INFO && console.groupEnd("Generated Map " + seed);
+
+ // Return all the generated data
+ return { seed, grid, pack, mapCoordinates };
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/biomes.js b/procedural/src/engine/modules/biomes.js
new file mode 100644
index 00000000..449c995e
--- /dev/null
+++ b/procedural/src/engine/modules/biomes.js
@@ -0,0 +1,129 @@
+"use strict";
+
+const MIN_LAND_HEIGHT = 20;
+
+export 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 [>19C; <-4C]; 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, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 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: Array.from({length: name.length}, (_, i) => i), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
+};
+
+// assign biome id for each cell
+export function define(pack, grid, config, utils) {
+ const {TIME, d3, rn} = utils;
+ TIME && console.time("defineBiomes");
+
+ const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
+ const {temp, prec} = grid.cells;
+ const biome = new Uint8Array(pack.cells.i.length); // biomes array
+ const biomesData = getDefault();
+
+ 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]];
+ biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId]), biomesData);
+ }
+
+ 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");
+ return {biome};
+}
+
+export function getId(moisture, temperature, height, hasRiver, biomesData = null) {
+ const data = biomesData || getDefault();
+
+ 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 data.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;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/burgs-and-states.js b/procedural/src/engine/modules/burgs-and-states.js
new file mode 100644
index 00000000..05cf3932
--- /dev/null
+++ b/procedural/src/engine/modules/burgs-and-states.js
@@ -0,0 +1,893 @@
+"use strict";
+
+export const generate = (pack, grid, config, utils) => {
+
+ const {cells, cultures} = pack;
+ const n = cells.i.length;
+
+ const newCells = {...cells, burg: new Uint16Array(n)};
+ const newPack = {...pack, cells: newCells};
+
+ const burgs = placeCapitals(newPack, grid, config, utils);
+ const states = createStates(newPack, burgs, config, utils);
+
+ placeTowns(newPack, burgs, grid, config, utils);
+ expandStates(newPack, grid, config, utils);
+ normalizeStates(newPack, utils);
+ getPoles(newPack, utils);
+
+ specifyBurgs(newPack, grid, utils);
+
+ collectStatistics(newPack);
+ assignColors(newPack, utils);
+
+ generateCampaigns(newPack, utils);
+ generateDiplomacy(newPack, utils);
+
+ return {
+ burgs: newPack.burgs,
+ states: newPack.states,
+ cells: {
+ ...pack.cells,
+ burg: newPack.cells.burg,
+ state: newPack.cells.state
+ }
+ };
+};
+
+function placeCapitals(pack, grid, config, utils) {
+ const {TIME, WARN, d3, graphWidth, graphHeight} = utils;
+ TIME && console.time("placeCapitals");
+ let count = config.statesNumber;
+ let burgs = [0];
+
+ const {cells} = pack;
+ const rand = () => 0.5 + Math.random() * 0.5;
+ const score = new Int16Array(cells.s.map(s => s * rand())); // cell score for capitals placement
+ const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ if (sorted.length < count * 10) {
+ count = Math.floor(sorted.length / 10);
+ if (!count) {
+ WARN && console.warn("There is no populated cells. Cannot generate states");
+ return burgs;
+ } else {
+ WARN && console.warn(`Not enough populated cells (${sorted.length}). Will generate only ${count} states`);
+ }
+ }
+
+ let burgsTree = d3.quadtree();
+ let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
+
+ for (let i = 0; burgs.length <= count; i++) {
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+
+ if (burgsTree.find(x, y, spacing) === undefined) {
+ burgs.push({cell, x, y});
+ burgsTree.add([x, y]);
+ }
+
+ if (i === sorted.length - 1) {
+ WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
+ burgsTree = d3.quadtree();
+ i = -1;
+ burgs = [0];
+ spacing /= 1.2;
+ }
+ }
+
+ burgs[0] = burgsTree;
+ TIME && console.timeEnd("placeCapitals");
+ return burgs;
+}
+
+// For each capital create a state
+function createStates(pack, burgs, config, utils) {
+ const {TIME, rn, each, Names, COA, getColors} = utils;
+ TIME && console.time("createStates");
+ const {cells, cultures} = pack;
+ const states = [{i: 0, name: "Neutrals"}];
+ const colors = getColors(burgs.length - 1);
+ const each5th = each(5);
+
+ burgs.forEach((b, i) => {
+ if (!i) return; // skip first element
+
+ // burgs data
+ b.i = b.state = i;
+ b.culture = cells.culture[b.cell];
+ b.name = Names.getCultureShort(b.culture);
+ b.feature = cells.f[b.cell];
+ b.capital = 1;
+
+ // states data
+ const expansionism = rn(Math.random() * config.sizeVariety + 1, 1);
+ const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
+ const name = Names.getState(basename, b.culture);
+ const type = cultures[b.culture].type;
+
+ const coa = COA.generate(null, null, null, type);
+ coa.shield = COA.getShield(b.culture, null);
+ states.push({
+ i,
+ color: colors[i - 1],
+ name,
+ expansionism,
+ capital: i,
+ type,
+ center: b.cell,
+ culture: b.culture,
+ coa
+ });
+ cells.burg[b.cell] = i;
+ });
+
+ TIME && console.timeEnd("createStates");
+ return states;
+}
+
+// place secondary settlements based on geo and economical evaluation
+function placeTowns(pack, burgs, grid, config, utils) {
+ const {TIME, ERROR, rn, gauss, Names, graphWidth, graphHeight} = utils;
+ TIME && console.time("placeTowns");
+ const {cells} = pack;
+ const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
+ const sorted = cells.i
+ .filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i])
+ .sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ const desiredNumber =
+ config.manorsInput == 1000
+ ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8)
+ : config.manorsInput;
+ const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
+ let burgsAdded = 0;
+
+ const burgsTree = burgs[0];
+ let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between towns
+
+ while (burgsAdded < burgsNumber && spacing > 1) {
+ for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
+ if (cells.burg[sorted[i]]) continue;
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+ const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
+ if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
+ const burg = burgs.length;
+ const culture = cells.culture[cell];
+ const name = Names.getCulture(culture);
+ burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature: cells.f[cell]});
+ burgsTree.add([x, y]);
+ cells.burg[cell] = burg;
+ burgsAdded++;
+ }
+ spacing *= 0.5;
+ }
+
+ if (config.manorsInput != 1000 && burgsAdded < desiredNumber) {
+ ERROR && console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
+ }
+
+ burgs[0] = {name: undefined}; // do not store burgsTree anymore
+ TIME && console.timeEnd("placeTowns");
+}
+
+// define burg coordinates, coa, port status and define details
+export const specifyBurgs = (pack, grid, utils) => {
+ const {TIME, rn, gauss, P, COA} = utils;
+ TIME && console.time("specifyBurgs");
+ const {cells, features} = pack;
+ const temp = grid.cells.temp;
+
+ for (const b of pack.burgs) {
+ if (!b.i || b.lock) continue;
+ const i = b.cell;
+
+ // asign port status to some coastline burgs with temp > 0 °C
+ const haven = cells.haven[i];
+ if (haven && temp[cells.g[i]] > 0) {
+ const f = cells.f[haven]; // water body id
+ // port is a capital with any harbor OR town with good harbor
+ const port = features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
+ b.port = port ? f : 0; // port is defined by water body id it lays on
+ } else b.port = 0;
+
+ // define burg population (keep urbanization at about 10% rate)
+ b.population = rn(Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
+ if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
+
+ if (b.port) {
+ b.population = b.population * 1.3; // increase port population
+ const [x, y] = getCloseToEdgePoint(i, haven, pack, utils);
+ b.x = x;
+ b.y = y;
+ }
+
+ // add random factor
+ b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
+
+ // shift burgs on rivers semi-randomly and just a bit
+ if (!b.port && cells.r[i]) {
+ const shift = Math.min(cells.fl[i] / 150, 1);
+ if (i % 2) b.x = rn(b.x + shift, 2);
+ else b.x = rn(b.x - shift, 2);
+ if (cells.r[i] % 2) b.y = rn(b.y + shift, 2);
+ else b.y = rn(b.y - shift, 2);
+ }
+
+ // define emblem
+ const state = pack.states[b.state];
+ const stateCOA = state.coa;
+ let kinship = 0.25;
+ if (b.capital) kinship += 0.1;
+ else if (b.port) kinship -= 0.1;
+ if (b.culture !== state.culture) kinship -= 0.25;
+ b.type = getType(i, b.port, pack, utils);
+ const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
+ b.coa = COA.generate(stateCOA, kinship, null, type);
+ b.coa.shield = COA.getShield(b.culture, b.state);
+ }
+
+ // de-assign port status if it's the only one on feature
+ const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
+ for (const f of features) {
+ if (!f.i || f.land || f.border) continue;
+ const featurePorts = ports.filter(b => b.port === f.i);
+ if (featurePorts.length === 1) featurePorts[0].port = 0;
+ }
+
+ TIME && console.timeEnd("specifyBurgs");
+};
+
+export function getCloseToEdgePoint(cell1, cell2, pack, utils) {
+ const {cells, vertices} = pack;
+ const {rn} = utils;
+
+ 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];
+}
+
+export const getType = (cellId, port, pack, utils) => {
+ const {cells, features, burgs} = 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";
+};
+
+export const defineBurgFeatures = (burg, pack, utils) => {
+ const {P} = utils;
+ const {cells} = pack;
+
+ pack.burgs
+ .filter(b => (burg ? b.i == burg.i : b.i && !b.removed && !b.lock))
+ .forEach(b => {
+ const pop = b.population;
+ b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
+ b.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
+ b.walls = Number(b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
+ b.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)));
+ const religion = cells.religion[b.cell];
+ const theocracy = pack.states[b.state].form === "Theocracy";
+ b.temple = Number(
+ (religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
+ );
+ });
+};
+
+// expand cultures across the map (Dijkstra-like algorithm)
+export const expandStates = (pack, grid, config, utils) => {
+ const {TIME, FlatQueue, minmax, biomesData} = utils;
+ 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 = config.growthRate || 1;
+ const statesGrowthRate = config.statesGrowthRate || 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");
+};
+
+export const normalizeStates = (pack, utils) => {
+ const {TIME} = utils;
+ 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
+export const getPoles = (pack, utils) => {
+ const {getPolesOfInaccessibility} = utils;
+ const getType = cellId => pack.cells.state[cellId];
+ const poles = getPolesOfInaccessibility(pack, getType);
+
+ pack.states.forEach(s => {
+ if (!s.i || s.removed) return;
+ s.pole = poles[s.i] || [0, 0];
+ });
+};
+
+// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
+export const updateCultures = (pack, utils) => {
+ const {TIME} = utils;
+ TIME && console.time("updateCulturesForBurgsAndStates");
+
+ // Assign the culture associated with the burgs cell
+ pack.burgs = pack.burgs.map((burg, index) => {
+ if (index === 0) return burg;
+ return {...burg, culture: pack.cells.culture[burg.cell]};
+ });
+
+ // Assign the culture associated with the states' center cell
+ pack.states = pack.states.map((state, index) => {
+ if (index === 0) return state;
+ return {...state, culture: pack.cells.culture[state.center]};
+ });
+
+ TIME && console.timeEnd("updateCulturesForBurgsAndStates");
+};
+
+// calculate states data like area, population etc.
+export const collectStatistics = (pack) => {
+ const {cells, states} = pack;
+
+ states.forEach(s => {
+ if (s.removed) return;
+ s.cells = s.area = s.burgs = s.rural = s.urban = 0;
+ s.neighbors = new Set();
+ });
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue;
+ const s = cells.state[i];
+
+ // check for neighboring states
+ cells.c[i]
+ .filter(c => cells.h[c] >= 20 && cells.state[c] !== s)
+ .forEach(c => states[s].neighbors.add(cells.state[c]));
+
+ // 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++;
+ }
+ }
+
+ // convert neighbors Set object into array
+ states.forEach(s => {
+ if (!s.neighbors) return;
+ s.neighbors = Array.from(s.neighbors);
+ });
+};
+
+export const assignColors = (pack, utils) => {
+ const {TIME, getRandomColor, getMixedColor} = utils;
+ TIME && console.time("assignColors");
+ const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
+
+ // assign basic color using greedy coloring algorithm
+ pack.states.forEach(s => {
+ if (!s.i || s.removed || s.lock) return;
+ const neibs = s.neighbors;
+ s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
+ if (!s.color) s.color = getRandomColor();
+ colors.push(colors.shift());
+ });
+
+ // randomize each already used color a bit
+ colors.forEach(c => {
+ const sameColored = pack.states.filter(s => s.color === c && !s.lock);
+ sameColored.forEach((s, d) => {
+ if (!d) return;
+ s.color = getMixedColor(s.color);
+ });
+ });
+
+ TIME && console.timeEnd("assignColors");
+};
+
+const wars = {
+ War: 6,
+ Conflict: 2,
+ Campaign: 4,
+ Invasion: 2,
+ Rebellion: 2,
+ Conquest: 2,
+ Intervention: 1,
+ Expedition: 1,
+ Crusade: 1
+};
+
+export const generateCampaign = (state, pack, utils) => {
+ const {P, gauss, rw, getAdjective, Names, options} = utils;
+ 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
+export const generateCampaigns = (pack, utils) => {
+ pack.states.forEach(s => {
+ if (!s.i || s.removed) return;
+ s.campaigns = generateCampaign(s, pack, utils);
+ });
+};
+
+// generate Diplomatic Relationships
+export const generateDiplomacy = (pack, utils) => {
+ const {TIME, d3, P, ra, gauss, rw, trimVowels, options} = utils;
+ 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
+export const defineStateForms = (list, pack, utils) => {
+ const {TIME, d3, P, rw, rand, trimVowels, getAdjective} = utils;
+ 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, pack, utils);
+ s.fullName = getFullName(s, utils);
+ }
+
+ function selectForm(s, tier, pack, utils) {
+ const {P, rand, rw, trimVowels} = utils;
+ 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"
+];
+
+export const getFullName = (state, utils) => {
+ const {getAdjective} = utils;
+ 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}`;
+};
\ No newline at end of file
diff --git a/procedural/src/engine/modules/coa-generator.js b/procedural/src/engine/modules/coa-generator.js
new file mode 100644
index 00000000..814e5da2
--- /dev/null
+++ b/procedural/src/engine/modules/coa-generator.js
@@ -0,0 +1,2218 @@
+"use strict";
+
+// Import required utility functions
+import { P } from "../utils/probabilityUtils.js";
+
+const tinctures = {
+ field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1},
+ division: {metals: 5, colours: 8, stains: +P(0.03), patterns: 1},
+ charge: {metals: 2, colours: 3, stains: +P(0.05), patterns: 0},
+ metals: {argent: 3, or: 2},
+ colours: {gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2},
+ stains: {murrey: 1, sanguine: 1, tenné: 1},
+ patterns: {
+ semy: 8,
+ ermine: 6,
+ vair: 4,
+ counterVair: 1,
+ vairInPale: 1,
+ vairEnPointe: 2,
+ vairAncien: 2,
+ potent: 2,
+ counterPotent: 1,
+ potentInPale: 1,
+ potentEnPointe: 1,
+ chequy: 8,
+ lozengy: 5,
+ fusily: 2,
+ pally: 8,
+ barry: 10,
+ gemelles: 1,
+ bendy: 8,
+ bendySinister: 4,
+ palyBendy: 2,
+ barryBendy: 1,
+ pappellony: 2,
+ pappellony2: 3,
+ scaly: 1,
+ plumetty: 1,
+ masoned: 6,
+ fretty: 3,
+ grillage: 1,
+ chainy: 1,
+ maily: 2,
+ honeycombed: 1
+ }
+};
+
+const chargeData = {
+ agnusDei: {
+ colors: 2,
+ sinister: true
+ },
+ angel: {
+ colors: 2,
+ positions: {e: 1}
+ },
+ anvil: {
+ sinister: true
+ },
+ apple: {
+ colors: 2
+ },
+ arbalest: {
+ colors: 3,
+ reversed: true
+ },
+ archer: {
+ colors: 3,
+ sinister: true
+ },
+ armEmbowedHoldingSabre: {
+ colors: 3,
+ sinister: true
+ },
+ armEmbowedVambraced: {
+ sinister: true
+ },
+ armEmbowedVambracedHoldingSword: {
+ colors: 3,
+ sinister: true
+ },
+ armillarySphere: {
+ positions: {e: 1}
+ },
+ arrow: {
+ colors: 3,
+ reversed: true
+ },
+ arrowsSheaf: {
+ colors: 3,
+ reversed: true
+ },
+ axe: {
+ colors: 2,
+ sinister: true
+ },
+ badgerStatant: {
+ colors: 2,
+ sinister: true
+ },
+ banner: {
+ colors: 2
+ },
+ basilisk: {
+ colors: 3,
+ sinister: true
+ },
+ bearPassant: {
+ colors: 3,
+ sinister: true
+ },
+ bearRampant: {
+ colors: 3,
+ sinister: true
+ },
+ bee: {
+ colors: 3,
+ reversed: true
+ },
+ bell: {
+ colors: 2
+ },
+ boarHeadErased: {
+ colors: 3,
+ sinister: true
+ },
+ boarRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 12, beh: 1, kn: 1, jln: 2}
+ },
+ boat: {
+ colors: 2
+ },
+ bookClosed: {
+ colors: 3,
+ sinister: true
+ },
+ bookClosed2: {
+ sinister: true
+ },
+ bookOpen: {
+ colors: 3
+ },
+ bow: {
+ sinister: true
+ },
+ bowWithArrow: {
+ colors: 3,
+ reversed: true
+ },
+ bowWithThreeArrows: {
+ colors: 3
+ },
+ bucket: {
+ colors: 2
+ },
+ bugleHorn: {
+ colors: 2
+ },
+ bugleHorn2: {
+ colors: 2
+ },
+ bullHeadCaboshed: {
+ colors: 2
+ },
+ bullPassant: {
+ colors: 3,
+ sinister: true
+ },
+ butterfly: {
+ colors: 3,
+ reversed: true
+ },
+ camel: {
+ colors: 2,
+ sinister: true
+ },
+ cancer: {
+ reversed: true
+ },
+ cannon: {
+ colors: 2,
+ sinister: true
+ },
+ caravel: {
+ colors: 3,
+ sinister: true
+ },
+ castle: {
+ colors: 2
+ },
+ castle2: {
+ colors: 3
+ },
+ catPassantGuardant: {
+ colors: 2,
+ sinister: true
+ },
+ cavalier: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ centaur: {
+ colors: 3,
+ sinister: true
+ },
+ chalice: {
+ colors: 2
+ },
+ cinquefoil: {
+ reversed: true
+ },
+ cock: {
+ colors: 3,
+ sinister: true
+ },
+ comet: {
+ reversed: true
+ },
+ cowStatant: {
+ colors: 3,
+ sinister: true
+ },
+ cossack: {
+ colors: 3,
+ sinister: true
+ },
+ crescent: {
+ reversed: true
+ },
+ crocodile: {
+ colors: 2,
+ sinister: true
+ },
+ crosier: {
+ sinister: true
+ },
+ crossbow: {
+ colors: 3,
+ sinister: true
+ },
+ crossGamma: {
+ sinister: true
+ },
+ crossLatin: {
+ reversed: true
+ },
+ crossTau: {
+ reversed: true
+ },
+ crossTriquetra: {
+ reversed: true
+ },
+ crown: {
+ colors: 2,
+ positions: {
+ e: 10,
+ abcdefgzi: 1,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 1,
+ pq: 2,
+ abc: 1,
+ jln: 4,
+ jleh: 1,
+ def: 2,
+ abcpqh: 3
+ }
+ },
+ crown2: {
+ colors: 3,
+ positions: {
+ e: 10,
+ abcdefgzi: 1,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 1,
+ pq: 2,
+ abc: 1,
+ jln: 4,
+ jleh: 1,
+ def: 2,
+ abcpqh: 3
+ }
+ },
+ deerHeadCaboshed: {
+ colors: 2
+ },
+ dolphin: {
+ colors: 2,
+ sinister: true
+ },
+ donkeyHeadCaboshed: {
+ colors: 2
+ },
+ dove: {
+ colors: 2,
+ natural: "argent",
+ sinister: true
+ },
+ doveDisplayed: {
+ colors: 2,
+ natural: "argent",
+ sinister: true
+ },
+ dragonfly: {
+ colors: 2,
+ reversed: true
+ },
+ dragonPassant: {
+ colors: 3,
+ sinister: true
+ },
+ dragonRampant: {
+ colors: 3,
+ sinister: true
+ },
+ drakkar: {
+ colors: 3,
+ sinister: true
+ },
+ drawingCompass: {
+ sinister: true
+ },
+ drum: {
+ colors: 3
+ },
+ duck: {
+ colors: 3,
+ sinister: true
+ },
+ eagle: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1}
+ },
+ eagleTwoHeads: {
+ colors: 3
+ },
+ elephant: {
+ colors: 2,
+ sinister: true
+ },
+ elephantHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ falchion: {
+ colors: 2,
+ reversed: true
+ },
+ falcon: {
+ colors: 3,
+ sinister: true
+ },
+ fan: {
+ colors: 2,
+ reversed: true
+ },
+ fasces: {
+ colors: 3,
+ sinister: true
+ },
+ feather: {
+ sinister: true
+ },
+ flamberge: {
+ colors: 2,
+ reversed: true
+ },
+ flangedMace: {
+ reversed: true
+ },
+ fly: {
+ colors: 3,
+ reversed: true
+ },
+ foot: {
+ sinister: true
+ },
+ fountain: {
+ natural: "azure"
+ },
+ frog: {
+ reversed: true
+ },
+ garb: {
+ colors: 2,
+ natural: "or",
+ positions: {e: 1, def: 3, abc: 2, beh: 1, kn: 1, jln: 3, jleh: 1, abcpqh: 1, joe: 1, lme: 1}
+ },
+ gauntlet: {
+ sinister: true,
+ reversed: true
+ },
+ goat: {
+ colors: 3,
+ sinister: true
+ },
+ goutte: {
+ reversed: true
+ },
+ grapeBunch: {
+ colors: 3,
+ sinister: true
+ },
+ grapeBunch2: {
+ colors: 3,
+ sinister: true
+ },
+ grenade: {
+ colors: 2
+ },
+ greyhoundCourant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ greyhoundRampant: {
+ colors: 2,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ greyhoundSejant: {
+ colors: 3,
+ sinister: true
+ },
+ griffinPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1}
+ },
+ griffinRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ hand: {
+ sinister: true,
+ reversed: true,
+ positions: {e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1}
+ },
+ harp: {
+ colors: 2,
+ sinister: true
+ },
+ hatchet: {
+ colors: 2,
+ sinister: true
+ },
+ head: {
+ colors: 2,
+ sinister: true,
+ positions: {e: 1}
+ },
+ headWreathed: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ hedgehog: {
+ colors: 3,
+ sinister: true
+ },
+ helmet: {
+ sinister: true
+ },
+ helmetCorinthian: {
+ colors: 3,
+ sinister: true
+ },
+ helmetGreat: {
+ sinister: true
+ },
+ helmetZischagge: {
+ sinister: true
+ },
+ heron: {
+ colors: 2,
+ sinister: true
+ },
+ hindStatant: {
+ colors: 2,
+ sinister: true
+ },
+ hook: {
+ sinister: true
+ },
+ horseHeadCouped: {
+ sinister: true
+ },
+ horsePassant: {
+ colors: 2,
+ sinister: true
+ },
+ horseRampant: {
+ colors: 3,
+ sinister: true
+ },
+ horseSalient: {
+ colors: 2,
+ sinister: true
+ },
+ horseshoe: {
+ reversed: true
+ },
+ hourglass: {
+ colors: 3
+ },
+ ladybird: {
+ colors: 3,
+ reversed: true
+ },
+ lamb: {
+ colors: 2,
+ sinister: true
+ },
+ lambPassantReguardant: {
+ colors: 2,
+ sinister: true
+ },
+ lanceWithBanner: {
+ colors: 3,
+ sinister: true
+ },
+ laurelWreath: {
+ colors: 2
+ },
+ lighthouse: {
+ colors: 3
+ },
+ lionHeadCaboshed: {
+ colors: 2
+ },
+ lionHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ lionPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ lionPassantGuardant: {
+ colors: 3,
+ sinister: true
+ },
+ lionRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1}
+ },
+ lionSejant: {
+ colors: 3,
+ sinister: true
+ },
+ lizard: {
+ reversed: true
+ },
+ lochaberAxe: {
+ colors: 2,
+ sinister: true
+ },
+ log: {
+ sinister: true
+ },
+ lute: {
+ colors: 2,
+ sinister: true
+ },
+ lymphad: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ mace: {
+ colors: 2
+ },
+ maces: {
+ colors: 2
+ },
+ mallet: {
+ colors: 2
+ },
+ mantle: {
+ colors: 3
+ },
+ martenCourant: {
+ colors: 3,
+ sinister: true
+ },
+ mascle: {
+ positions: {
+ e: 15,
+ abcdefgzi: 3,
+ beh: 3,
+ bdefh: 4,
+ acegi: 1,
+ kn: 3,
+ joe: 2,
+ abc: 3,
+ jlh: 8,
+ jleh: 1,
+ df: 3,
+ abcpqh: 4,
+ pqe: 3,
+ eknpq: 3
+ }
+ },
+ mastiffStatant: {
+ colors: 3,
+ sinister: true
+ },
+ mitre: {
+ colors: 3
+ },
+ monk: {
+ sinister: true
+ },
+ moonInCrescent: {
+ sinister: true
+ },
+ mullet: {
+ reversed: true
+ },
+ mullet7: {
+ reversed: true
+ },
+ oak: {
+ colors: 3
+ },
+ orb: {
+ colors: 3
+ },
+ ouroboros: {
+ sinister: true
+ },
+ owl: {
+ colors: 2,
+ sinister: true
+ },
+ owlDisplayed: {
+ colors: 2
+ },
+ palmTree: {
+ colors: 3
+ },
+ parrot: {
+ colors: 2,
+ sinister: true
+ },
+ peacock: {
+ colors: 3,
+ sinister: true
+ },
+ peacockInPride: {
+ colors: 3,
+ sinister: true
+ },
+ pear: {
+ colors: 2
+ },
+ pegasus: {
+ colors: 3,
+ sinister: true
+ },
+ pike: {
+ colors: 2,
+ sinister: true
+ },
+ pineTree: {
+ colors: 2
+ },
+ plaice: {
+ colors: 2,
+ sinister: true
+ },
+ plough: {
+ colors: 2,
+ sinister: true
+ },
+ ploughshare: {
+ sinister: true
+ },
+ porcupine: {
+ colors: 2,
+ sinister: true
+ },
+ portcullis: {
+ colors: 2
+ },
+ rabbitSejant: {
+ colors: 2,
+ sinister: true
+ },
+ rake: {
+ reversed: true
+ },
+ rapier: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ ramHeadErased: {
+ colors: 3,
+ sinister: true
+ },
+ ramPassant: {
+ colors: 3,
+ sinister: true
+ },
+ ratRampant: {
+ colors: 2,
+ sinister: true
+ },
+ raven: {
+ colors: 2,
+ natural: "sable",
+ sinister: true,
+ positions: {e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1}
+ },
+ rhinoceros: {
+ colors: 2,
+ sinister: true
+ },
+ rose: {
+ colors: 3
+ },
+ sabre: {
+ colors: 2,
+ sinister: true
+ },
+ sabre2: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ sabresCrossed: {
+ colors: 2,
+ reversed: true
+ },
+ sagittarius: {
+ colors: 3,
+ sinister: true
+ },
+ salmon: {
+ colors: 2,
+ sinister: true
+ },
+ saw: {
+ colors: 2
+ },
+ scale: {
+ colors: 2
+ },
+ scaleImbalanced: {
+ colors: 2,
+ sinister: true
+ },
+ scissors: {
+ reversed: true
+ },
+ scorpion: {
+ reversed: true
+ },
+ scrollClosed: {
+ colors: 2,
+ sinister: true
+ },
+ scythe: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ scythe2: {
+ sinister: true
+ },
+ serpent: {
+ colors: 2,
+ sinister: true
+ },
+ shield: {
+ colors: 2,
+ sinister: true
+ },
+ sickle: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ snail: {
+ colors: 2,
+ sinister: true
+ },
+ snake: {
+ colors: 2,
+ sinister: true
+ },
+ spear: {
+ colors: 2,
+ reversed: true
+ },
+ spiral: {
+ sinister: true,
+ reversed: true
+ },
+ squirrel: {
+ sinister: true
+ },
+ stagLodgedRegardant: {
+ colors: 3,
+ sinister: true
+ },
+ stagPassant: {
+ colors: 2,
+ sinister: true
+ },
+ stirrup: {
+ colors: 2
+ },
+ swallow: {
+ colors: 2,
+ sinister: true
+ },
+ swan: {
+ colors: 3,
+ sinister: true
+ },
+ swanErased: {
+ colors: 3,
+ sinister: true
+ },
+ sword: {
+ colors: 2,
+ reversed: true
+ },
+ talbotPassant: {
+ colors: 3,
+ sinister: true
+ },
+ talbotSejant: {
+ colors: 3,
+ sinister: true
+ },
+ tower: {
+ colors: 2
+ },
+ tree: {
+ positions: {e: 1}
+ },
+ trefoil: {
+ reversed: true
+ },
+ trowel: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ unicornRampant: {
+ colors: 3,
+ sinister: true
+ },
+ wasp: {
+ colors: 3,
+ reversed: true
+ },
+ wheatStalk: {
+ colors: 2
+ },
+ windmill: {
+ colors: 3,
+ sinister: true
+ },
+ wing: {
+ sinister: true
+ },
+ wingSword: {
+ colors: 3,
+ sinister: true
+ },
+ wolfHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ wolfPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ wolfRampant: {
+ colors: 3,
+ sinister: true
+ },
+ wolfStatant: {
+ colors: 3,
+ sinister: true
+ },
+ wyvern: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, jln: 1}
+ },
+ wyvernWithWingsDisplayed: {
+ colors: 3,
+ sinister: true
+ }
+};
+
+const charges = {
+ types: {
+ conventional: 33, // 40 charges
+ crosses: 13, // 30 charges
+ beasts: 7, // 41 charges
+ beastHeads: 3, // 10 charges
+ birds: 3, // 16 charges
+ reptiles: 2, // 5 charges
+ bugs: 2, // 8 charges
+ fishes: 1, // 3 charges
+ molluscs: 1, // 2 charges
+ plants: 3, // 18 charges
+ fantastic: 5, // 14 charges
+ agriculture: 2, // 8 charges
+ arms: 5, // 32 charges
+ bodyparts: 2, // 12 charges
+ people: 2, // 4 charges
+ architecture: 3, // 11 charges
+ seafaring: 3, // 9 charges
+ tools: 3, // 15 charges
+ miscellaneous: 5, // 30 charges
+ inescutcheon: 3, // 43 charges
+ ornaments: 0, // 9 charges
+ uploaded: 0
+ },
+ single: {
+ conventional: 10,
+ crosses: 8,
+ beasts: 7,
+ beastHeads: 3,
+ birds: 3,
+ reptiles: 2,
+ bugs: 2,
+ fishes: 1,
+ molluscs: 1,
+ plants: 3,
+ fantastic: 5,
+ agriculture: 2,
+ arms: 5,
+ bodyparts: 2,
+ people: 2,
+ architecture: 3,
+ seafaring: 3,
+ tools: 3,
+ miscellaneous: 5,
+ inescutcheon: 1
+ },
+ semy: {
+ conventional: 4,
+ crosses: 1
+ },
+ conventional: {
+ annulet: 4,
+ billet: 5,
+ carreau: 1,
+ comet: 1,
+ compassRose: 1,
+ crescent: 5,
+ delf: 0,
+ estoile: 1,
+ fleurDeLis: 6,
+ fountain: 1,
+ fusil: 4,
+ gear: 1,
+ goutte: 4,
+ heart: 4,
+ lozenge: 2,
+ lozengeFaceted: 3,
+ lozengePloye: 1,
+ mascle: 4,
+ moonInCrescent: 1,
+ mullet: 5,
+ mullet10: 1,
+ mullet4: 3,
+ mullet6: 4,
+ mullet6Faceted: 1,
+ mullet6Pierced: 1,
+ mullet7: 1,
+ mullet8: 1,
+ mulletFaceted: 1,
+ mulletPierced: 1,
+ pique: 2,
+ roundel: 4,
+ roundel2: 3,
+ rustre: 2,
+ spiral: 1,
+ sun: 3,
+ sunInSplendour: 1,
+ sunInSplendour2: 1,
+ trefle: 2,
+ triangle: 3,
+ trianglePierced: 1
+ },
+ crosses: {
+ crossHummetty: 15,
+ crossVoided: 1,
+ crossPattee: 2,
+ crossPatteeAlisee: 1,
+ crossFormee: 1,
+ crossFormee2: 2,
+ crossPotent: 2,
+ crossJerusalem: 1,
+ crosslet: 1,
+ crossClechy: 3,
+ crossBottony: 1,
+ crossFleury: 3,
+ crossPatonce: 1,
+ crossPommy: 1,
+ crossGamma: 1,
+ crossArrowed: 1,
+ crossFitchy: 1,
+ crossCercelee: 1,
+ crossMoline: 2,
+ crossFourchy: 1,
+ crossAvellane: 1,
+ crossErminee: 1,
+ crossBiparted: 1,
+ crossMaltese: 3,
+ crossTemplar: 2,
+ crossCeltic: 1,
+ crossCeltic2: 1,
+ crossTriquetra: 1,
+ crossCarolingian: 1,
+ crossOccitan: 1,
+ crossSaltire: 3,
+ crossBurgundy: 1,
+ crossLatin: 3,
+ crossPatriarchal: 1,
+ crossOrthodox: 1,
+ crossCalvary: 1,
+ crossDouble: 1,
+ crossTau: 1,
+ crossSantiago: 1,
+ crossAnkh: 1
+ },
+ beasts: {
+ agnusDei: 1,
+ badgerStatant: 1,
+ bearPassant: 1,
+ bearRampant: 3,
+ boarRampant: 1,
+ bullPassant: 1,
+ camel: 1,
+ catPassantGuardant: 1,
+ cowStatant: 1,
+ dolphin: 1,
+ elephant: 1,
+ goat: 1,
+ greyhoundCourant: 1,
+ greyhoundRampant: 1,
+ greyhoundSejant: 1,
+ hedgehog: 1,
+ hindStatant: 1,
+ horsePassant: 1,
+ horseRampant: 2,
+ horseSalient: 1,
+ lamb: 1,
+ lambPassantReguardant: 1,
+ lionPassant: 3,
+ lionPassantGuardant: 2,
+ lionRampant: 7,
+ lionSejant: 2,
+ martenCourant: 1,
+ mastiffStatant: 1,
+ porcupine: 1,
+ rabbitSejant: 1,
+ ramPassant: 1,
+ ratRampant: 1,
+ rhinoceros: 1,
+ squirrel: 1,
+ stagLodgedRegardant: 1,
+ stagPassant: 1,
+ talbotPassant: 1,
+ talbotSejant: 1,
+ wolfPassant: 1,
+ wolfRampant: 1,
+ wolfStatant: 1
+ },
+ beastHeads: {
+ boarHeadErased: 1,
+ bullHeadCaboshed: 1,
+ deerHeadCaboshed: 1,
+ donkeyHeadCaboshed: 1,
+ elephantHeadErased: 1,
+ horseHeadCouped: 1,
+ lionHeadCaboshed: 2,
+ lionHeadErased: 2,
+ ramHeadErased: 1,
+ wolfHeadErased: 2
+ },
+ birds: {
+ cock: 3,
+ dove: 2,
+ doveDisplayed: 1,
+ duck: 1,
+ eagle: 9,
+ falcon: 2,
+ heron: 1,
+ owl: 1,
+ owlDisplayed: 1,
+ parrot: 1,
+ peacock: 1,
+ peacockInPride: 1,
+ raven: 2,
+ swallow: 1,
+ swan: 2,
+ swanErased: 1
+ },
+ reptiles: {
+ crocodile: 1,
+ frog: 1,
+ lizard: 1,
+ ouroboros: 1,
+ snake: 1
+ },
+ bugs: {
+ bee: 1,
+ butterfly: 1,
+ cancer: 1,
+ dragonfly: 1,
+ fly: 1,
+ ladybird: 1,
+ scorpion: 1,
+ wasp: 1
+ },
+ fishes: {
+ pike: 1,
+ plaice: 1,
+ salmon: 1
+ },
+ molluscs: {
+ escallop: 4,
+ snail: 1
+ },
+ plants: {
+ apple: 1,
+ cinquefoil: 1,
+ earOfWheat: 1,
+ grapeBunch: 1,
+ grapeBunch2: 1,
+ mapleLeaf: 1,
+ oak: 1,
+ palmTree: 1,
+ pear: 1,
+ pineCone: 1,
+ pineTree: 1,
+ quatrefoil: 1,
+ rose: 1,
+ sextifoil: 1,
+ thistle: 1,
+ tree: 1,
+ trefoil: 1,
+ wheatStalk: 1
+ },
+ fantastic: {
+ angel: 3,
+ basilisk: 1,
+ centaur: 1,
+ dragonPassant: 3,
+ dragonRampant: 2,
+ eagleTwoHeads: 2,
+ griffinPassant: 1,
+ griffinRampant: 2,
+ pegasus: 1,
+ sagittarius: 1,
+ serpent: 1,
+ unicornRampant: 1,
+ wyvern: 1,
+ wyvernWithWingsDisplayed: 1
+ },
+ agriculture: {
+ garb: 2,
+ millstone: 1,
+ plough: 1,
+ ploughshare: 1,
+ rake: 1,
+ scythe: 1,
+ scythe2: 1,
+ sickle: 1
+ },
+ arms: {
+ arbalest: 1,
+ arbalest2: 1,
+ arrow: 1,
+ arrowsSheaf: 1,
+ axe: 3,
+ bow: 1,
+ bowWithArrow: 2,
+ bowWithThreeArrows: 1,
+ cannon: 1,
+ falchion: 1,
+ flamberge: 1,
+ flangedMace: 1,
+ gauntlet: 1,
+ grenade: 1,
+ hatchet: 3,
+ helmet: 2,
+ helmetCorinthian: 1,
+ helmetGreat: 2,
+ helmetZischagge: 1,
+ lanceHead: 1,
+ lanceWithBanner: 1,
+ lochaberAxe: 1,
+ mace: 1,
+ maces: 1,
+ mallet: 1,
+ rapier: 1,
+ sabre: 1,
+ sabre2: 1,
+ sabresCrossed: 1,
+ shield: 1,
+ spear: 1,
+ sword: 4
+ },
+ bodyparts: {
+ armEmbowedHoldingSabre: 1,
+ armEmbowedVambraced: 1,
+ armEmbowedVambracedHoldingSword: 1,
+ bone: 1,
+ crossedBones: 2,
+ foot: 1,
+ hand: 4,
+ head: 1,
+ headWreathed: 1,
+ skeleton: 2,
+ skull: 2,
+ skull2: 1
+ },
+ people: {
+ archer: 1,
+ cavalier: 3,
+ cossack: 1,
+ monk: 1
+ },
+ architecture: {
+ bridge: 1,
+ bridge2: 1,
+ castle: 2,
+ castle2: 1,
+ column: 1,
+ lighthouse: 1,
+ palace: 1,
+ pillar: 1,
+ portcullis: 1,
+ tower: 2,
+ windmill: 1
+ },
+ seafaring: {
+ anchor: 6,
+ armillarySphere: 1,
+ boat: 2,
+ boat2: 1,
+ caravel: 1,
+ drakkar: 1,
+ lymphad: 2,
+ raft: 1,
+ shipWheel: 1
+ },
+ tools: {
+ anvil: 2,
+ drawingCompass: 2,
+ fan: 1,
+ hook: 1,
+ ladder: 1,
+ ladder2: 1,
+ pincers: 1,
+ saw: 1,
+ scale: 1,
+ scaleImbalanced: 1,
+ scalesHanging: 1,
+ scissors: 1,
+ scissors2: 1,
+ shears: 1,
+ trowel: 1
+ },
+ miscellaneous: {
+ attire: 2,
+ banner: 2,
+ bell: 3,
+ bookClosed: 1,
+ bookClosed2: 1,
+ bookOpen: 1,
+ bucket: 1,
+ buckle: 1,
+ bugleHorn: 2,
+ bugleHorn2: 1,
+ chain: 2,
+ chalice: 2,
+ cowHorns: 3,
+ crosier: 1,
+ crown: 3,
+ crown2: 2,
+ drum: 1,
+ fasces: 1,
+ feather: 3,
+ harp: 2,
+ horseshoe: 3,
+ hourglass: 2,
+ key: 3,
+ laurelWreath: 2,
+ laurelWreath2: 1,
+ log: 1,
+ lute: 2,
+ lyre: 1,
+ mitre: 1,
+ orb: 1,
+ pot: 2,
+ ramsHorn: 1,
+ sceptre: 1,
+ scrollClosed: 1,
+ snowflake: 1,
+ stagsAttires: 1,
+ stirrup: 2,
+ wheel: 3,
+ wing: 2,
+ wingSword: 1
+ },
+ inescutcheon: {
+ inescutcheonHeater: 1,
+ inescutcheonSpanish: 1,
+ inescutcheonFrench: 1,
+ inescutcheonHorsehead: 1,
+ inescutcheonHorsehead2: 1,
+ inescutcheonPolish: 1,
+ inescutcheonHessen: 1,
+ inescutcheonSwiss: 1,
+ inescutcheonBoeotian: 1,
+ inescutcheonRoman: 1,
+ inescutcheonKite: 1,
+ inescutcheonOldFrench: 1,
+ inescutcheonRenaissance: 1,
+ inescutcheonBaroque: 1,
+ inescutcheonTarge: 1,
+ inescutcheonTarge2: 1,
+ inescutcheonPavise: 1,
+ inescutcheonWedged: 1,
+ inescutcheonFlag: 1,
+ inescutcheonPennon: 1,
+ inescutcheonGuidon: 1,
+ inescutcheonBanner: 1,
+ inescutcheonDovetail: 1,
+ inescutcheonGonfalon: 1,
+ inescutcheonPennant: 1,
+ inescutcheonRound: 1,
+ inescutcheonOval: 1,
+ inescutcheonVesicaPiscis: 1,
+ inescutcheonSquare: 1,
+ inescutcheonDiamond: 1,
+ inescutcheonNo: 1,
+ inescutcheonFantasy1: 1,
+ inescutcheonFantasy2: 1,
+ inescutcheonFantasy3: 1,
+ inescutcheonFantasy4: 1,
+ inescutcheonFantasy5: 1,
+ inescutcheonNoldor: 1,
+ inescutcheonGondor: 1,
+ inescutcheonEasterling: 1,
+ inescutcheonErebor: 1,
+ inescutcheonIronHills: 1,
+ inescutcheonUrukHai: 1,
+ inescutcheonMoriaOrc: 1
+ },
+ ornaments: {
+ mantle: 0,
+ ribbon1: 3,
+ ribbon2: 2,
+ ribbon3: 1,
+ ribbon4: 1,
+ ribbon5: 1,
+ ribbon6: 1,
+ ribbon7: 1,
+ ribbon8: 1
+ },
+ data: chargeData
+};
+
+// charges specific to culture or burg type (FMG-only config, not coming from Armoria)
+const typeMapping = {
+ Naval: {
+ anchor: 3,
+ drakkar: 1,
+ lymphad: 2,
+ caravel: 1,
+ shipWheel: 1,
+ armillarySphere: 1,
+ escallop: 1,
+ dolphin: 1,
+ plaice: 1
+ },
+ Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1},
+ River: {
+ garb: 1,
+ rake: 1,
+ raft: 1,
+ boat: 2,
+ drakkar: 2,
+ hook: 2,
+ pike: 2,
+ bullHeadCaboshed: 1,
+ apple: 1,
+ pear: 1,
+ plough: 1,
+ earOfWheat: 1,
+ salmon: 1,
+ cancer: 1,
+ bridge: 1,
+ bridge2: 2,
+ sickle: 1,
+ scythe: 1,
+ grapeBunch: 1,
+ wheatStalk: 1,
+ windmill: 1,
+ crocodile: 1
+ },
+ Lake: {
+ hook: 3,
+ cancer: 2,
+ escallop: 1,
+ pike: 2,
+ heron: 1,
+ boat: 1,
+ boat2: 2,
+ salmon: 1,
+ cancer: 1,
+ sickle: 1,
+ windmill: 1,
+ swanErased: 1,
+ swan: 1,
+ frog: 1,
+ wasp: 1
+ },
+ Nomadic: {
+ pot: 1,
+ buckle: 1,
+ wheel: 2,
+ sabre: 2,
+ sabresCrossed: 1,
+ bow: 2,
+ arrow: 1,
+ horseRampant: 1,
+ horseSalient: 1,
+ crescent: 1,
+ camel: 3,
+ scorpion: 1,
+ falcon: 1
+ },
+ Hunting: {
+ bugleHorn: 2,
+ bugleHorn2: 1,
+ stagsAttires: 2,
+ attire: 2,
+ hatchet: 1,
+ bowWithArrow: 2,
+ arrowsSheaf: 1,
+ lanceHead: 1,
+ saw: 1,
+ deerHeadCaboshed: 1,
+ wolfStatant: 1,
+ oak: 1,
+ pineCone: 1,
+ pineTree: 1,
+ oak: 1,
+ owl: 1,
+ falcon: 1,
+ peacock: 1,
+ boarHeadErased: 2,
+ horseHeadCouped: 1,
+ rabbitSejant: 1,
+ wolfRampant: 1,
+ wolfPassant: 1,
+ wolfStatant: 1,
+ greyhoundCourant: 1,
+ greyhoundRampant: 1,
+ greyhoundSejant: 1,
+ mastiffStatant: 1,
+ talbotPassant: 1,
+ talbotSejant: 1,
+ stagPassant: 21
+ },
+ // selection based on type
+ City: {
+ key: 4,
+ bell: 3,
+ lute: 1,
+ tower: 1,
+ pillar: 1,
+ castle: 1,
+ castle2: 1,
+ portcullis: 1,
+ mallet: 1,
+ cannon: 1,
+ anvil: 1,
+ buckle: 1,
+ horseshoe: 1,
+ stirrup: 1,
+ lanceWithBanner: 1,
+ bookClosed: 1,
+ scissors: 1,
+ scissors2: 1,
+ shears: 1,
+ pincers: 1,
+ bridge: 2,
+ archer: 1,
+ cannon: 1,
+ shield: 1,
+ arbalest: 1,
+ arbalest2: 1,
+ bowWithThreeArrows: 1,
+ spear: 1,
+ lochaberAxe: 1,
+ armEmbowedHoldingSabre: 1,
+ grenade: 1,
+ maces: 1,
+ grapeBunch: 1,
+ cock: 1,
+ ramHeadErased: 1,
+ ratRampant: 1,
+ hourglass: 1,
+ scale: 1,
+ scrollClosed: 1
+ },
+ Capital: {
+ crown: 2,
+ crown2: 2,
+ laurelWreath: 1,
+ orb: 1,
+ lute: 1,
+ lyre: 1,
+ banner: 1,
+ castle: 1,
+ castle2: 1,
+ palace: 1,
+ crown2: 2,
+ column: 1,
+ lionRampant: 1,
+ stagLodgedRegardant: 1,
+ drawingCompass: 1,
+ rapier: 1,
+ scaleImbalanced: 1,
+ scalesHanging: 1
+ },
+ Сathedra: {
+ crossHummetty: 3,
+ mitre: 3,
+ chalice: 1,
+ orb: 1,
+ crosier: 2,
+ lamb: 1,
+ monk: 2,
+ angel: 3,
+ crossLatin: 2,
+ crossPatriarchal: 1,
+ crossOrthodox: 1,
+ crossCalvary: 1,
+ agnusDei: 3,
+ bookOpen: 1,
+ sceptre: 1,
+ bone: 1,
+ skull: 1
+ }
+};
+
+const positions = {
+ conventional: {
+ e: 20,
+ abcdefgzi: 3,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 3,
+ bhdf: 1,
+ jeo: 1,
+ abc: 3,
+ jln: 6,
+ jlh: 3,
+ kmo: 2,
+ jleh: 1,
+ def: 3,
+ abcpqh: 4,
+ ABCDEFGHIJKL: 1
+ },
+ complex: {e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1},
+ divisions: {
+ perPale: {e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1},
+ perFess: {e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1},
+ perBend: {e: 5, lm: 5, bcfdgh: 1},
+ perBendSinister: {e: 1, jo: 1},
+ perCross: {e: 4, jlmo: 1, j: 1, jo: 2, jl: 1},
+ perChevron: {e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1},
+ perChevronReversed: {e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1},
+ perSaltire: {bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1},
+ perPile: {ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1}
+ },
+ inescutcheon: {e: 4, jln: 1}
+};
+
+const lines = {
+ straight: 50,
+ wavy: 8,
+ engrailed: 4,
+ invecked: 3,
+ rayonne: 3,
+ embattled: 1,
+ raguly: 1,
+ urdy: 1,
+ dancetty: 1,
+ indented: 2,
+ dentilly: 1,
+ bevilled: 1,
+ angled: 1,
+ flechy: 1,
+ barby: 1,
+ enclavy: 1,
+ escartely: 1,
+ arched: 2,
+ archedReversed: 1,
+ nowy: 1,
+ nowyReversed: 1,
+ embattledGhibellin: 1,
+ embattledNotched: 1,
+ embattledGrady: 1,
+ dovetailedIndented: 1,
+ dovetailed: 1,
+ potenty: 1,
+ potentyDexter: 1,
+ potentySinister: 1,
+ nebuly: 2,
+ seaWaves: 1,
+ dragonTeeth: 1,
+ firTrees: 1
+};
+
+const divisions = {
+ variants: {
+ perPale: 5,
+ perFess: 5,
+ perBend: 2,
+ perBendSinister: 1,
+ perChevron: 1,
+ perChevronReversed: 1,
+ perCross: 5,
+ perPile: 1,
+ perSaltire: 1,
+ gyronny: 1,
+ chevronny: 1
+ },
+ perPale: lines,
+ perFess: lines,
+ perBend: lines,
+ perBendSinister: lines,
+ perChevron: lines,
+ perChevronReversed: lines,
+ perCross: {
+ straight: 20,
+ wavy: 5,
+ engrailed: 4,
+ invecked: 3,
+ rayonne: 1,
+ embattled: 1,
+ raguly: 1,
+ urdy: 1,
+ indented: 2,
+ dentilly: 1,
+ bevilled: 1,
+ angled: 1,
+ embattledGhibellin: 1,
+ embattledGrady: 1,
+ dovetailedIndented: 1,
+ dovetailed: 1,
+ potenty: 1,
+ potentyDexter: 1,
+ potentySinister: 1,
+ nebuly: 1
+ },
+ perPile: lines
+};
+
+const ordinaries = {
+ lined: {
+ pale: 7,
+ fess: 5,
+ bend: 3,
+ bendSinister: 2,
+ chief: 5,
+ bar: 2,
+ gemelle: 1,
+ fessCotissed: 1,
+ fessDoubleCotissed: 1,
+ bendlet: 2,
+ bendletSinister: 1,
+ terrace: 3,
+ cross: 6,
+ crossParted: 1,
+ saltire: 2,
+ saltireParted: 1
+ },
+ straight: {
+ bordure: 8,
+ orle: 4,
+ mount: 1,
+ point: 2,
+ flaunches: 1,
+ gore: 1,
+ gyron: 1,
+ quarter: 1,
+ canton: 2,
+ pall: 3,
+ pallReversed: 2,
+ chevron: 4,
+ chevronReversed: 3,
+ pile: 2,
+ pileInBend: 2,
+ pileInBendSinister: 1,
+ piles: 1,
+ pilesInPoint: 2,
+ label: 1
+ },
+ data: {
+ bar: {
+ positionsOn: {defdefdef: 1},
+ positionsOff: {abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1}
+ },
+ bend: {
+ positionsOn: {ee: 2, jo: 1, joe: 1},
+ positionsOff: {ccg: 2, ccc: 1}
+ },
+ bendSinister: {
+ positionsOn: {ee: 1, lm: 1, lem: 4},
+ positionsOff: {aai: 2, aaa: 1}
+ },
+ bendlet: {
+ positionsOn: {joejoejoe: 1},
+ positionsOff: {ccg: 2, ccc: 1}
+ },
+ bendletSinister: {
+ positionsOn: {lemlemlem: 1},
+ positionsOff: {aai: 2, aaa: 1}
+ },
+ bordure: {
+ positionsOn: {ABCDEFGHIJKL: 1},
+ positionsOff: {e: 4, jleh: 2, kenken: 1, peqpeq: 1}
+ },
+ canton: {
+ positionsOn: {yyyy: 1},
+ positionsOff: {e: 5, beh: 1, def: 1, bdefh: 1, kn: 1}
+ },
+ chevron: {
+ positionsOn: {ach: 3, hhh: 1}
+ },
+ chevronReversed: {
+ positionsOff: {bbb: 1}
+ },
+ chief: {
+ positionsOn: {abc: 5, bbb: 1},
+ positionsOff: {emo: 2, emoz: 1, ez: 2}
+ },
+ cross: {
+ positionsOn: {eeee: 1, behdfbehdf: 3, behbehbeh: 2},
+ positionsOff: {acgi: 1}
+ },
+ crossParted: {
+ positionsOn: {e: 5, ee: 1}
+ },
+ fess: {
+ positionsOn: {ee: 1, def: 3},
+ positionsOff: {abc: 3, abcz: 1}
+ },
+ fessCotissed: {
+ positionsOn: {ee: 1, def: 3}
+ },
+ fessDoubleCotissed: {
+ positionsOn: {ee: 1, defdef: 3}
+ },
+ flaunches: {
+ positionsOff: {e: 3, kn: 1, beh: 3}
+ },
+ gemelle: {
+ positionsOff: {abc: 1}
+ },
+ gyron: {
+ positionsOff: {bh: 1}
+ },
+ label: {
+ positionsOff: {defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5}
+ },
+ mount: {
+ positionsOff: {e: 5, def: 1, bdf: 3}
+ },
+ orle: {
+ positionsOff: {e: 4, jleh: 1, kenken: 1, peqpeq: 1}
+ },
+ pale: {
+ positionsOn: {ee: 12, beh: 10, kn: 3, bb: 1},
+ positionsOff: {yyy: 1}
+ },
+ pall: {
+ positionsOn: {ee: 1, jleh: 5, jlhh: 3},
+ positionsOff: {BCKFEILGJbdmfo: 1}
+ },
+ pallReversed: {
+ positionsOn: {ee: 1, bemo: 5},
+ positionsOff: {aczac: 1}
+ },
+ pile: {
+ positionsOn: {bbb: 1},
+ positionsOff: {acdfgi: 1, acac: 1}
+ },
+ pileInBend: {
+ positionsOn: {eeee: 1, eeoo: 1},
+ positionsOff: {cg: 1}
+ },
+ pileInBendSinister: {
+ positionsOn: {eeee: 1, eemm: 1},
+ positionsOff: {ai: 1}
+ },
+ point: {
+ positionsOff: {e: 2, def: 1, bdf: 3, acbdef: 1}
+ },
+ quarter: {
+ positionsOn: {jjj: 1},
+ positionsOff: {e: 1}
+ },
+ saltire: {
+ positionsOn: {ee: 5, jlemo: 1}
+ },
+ saltireParted: {
+ positionsOn: {e: 5, ee: 1}
+ },
+ terrace: {
+ positionsOff: {e: 5, def: 1, bdf: 3}
+ }
+ }
+};
+
+const shields = {
+ types: {basic: 10, regional: 2, historical: 1, specific: 1, banner: 1, simple: 2, fantasy: 1, middleEarth: 0},
+ basic: {heater: 12, spanish: 6, french: 1},
+ regional: {horsehead: 1, horsehead2: 1, polish: 1, hessen: 1, swiss: 1},
+ historical: {boeotian: 1, roman: 2, kite: 1, oldFrench: 5, renaissance: 2, baroque: 2},
+ specific: {targe: 1, targe2: 0, pavise: 5, wedged: 10},
+ banner: {flag: 1, pennon: 0, guidon: 0, banner: 0, dovetail: 1, gonfalon: 5, pennant: 0},
+ simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
+ fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3},
+ middleEarth: {noldor: 1, gondor: 1, easterling: 1, erebor: 1, ironHills: 1, urukHai: 1, moriaOrc: 1}
+};
+
+export function generate(pack, parent, kinship, dominion, type, utils) {
+ const {P, rw} = utils;
+
+ if (!parent || parent.custom) {
+ parent = null;
+ kinship = 0;
+ dominion = 0;
+ }
+
+ let usedPattern = null;
+ let usedTinctures = [];
+
+ const t1 = P(kinship) ? parent.t1 : getTincture("field");
+ if (t1.includes("-")) usedPattern = t1;
+ const coa = {t1};
+
+ const addCharge = P(usedPattern ? 0.5 : 0.93); // 80% for charge
+ const linedOrdinary =
+ (addCharge && P(0.3)) || P(0.5)
+ ? parent?.ordinaries && P(kinship)
+ ? parent.ordinaries[0].ordinary
+ : rw(ordinaries.lined)
+ : null;
+
+ const ordinary =
+ (!addCharge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
+
+ const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
+
+ const divisioned = (() => {
+ if (rareDivided) return P(0.03);
+ if (addCharge && ordinary) return P(0.03);
+ if (addCharge) return P(0.3);
+ if (ordinary) return P(0.7);
+ return P(0.995);
+ })();
+
+ const division = (() => {
+ if (divisioned) {
+ if (parent?.division && P(kinship - 0.1)) return parent.division.division;
+ return rw(divisions.variants);
+ }
+ return null;
+ })();
+
+ if (division) {
+ const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
+ coa.division = {division, t};
+ if (divisions[division])
+ coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
+ }
+
+ if (ordinary) {
+ coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}];
+ if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? "straight" : rw(lines);
+ if (division && !addCharge && !usedPattern && P(0.5) && ordinary !== "bordure" && ordinary !== "orle") {
+ if (P(0.8)) coa.ordinaries[0].divided = "counter";
+ // 40%
+ else if (P(0.6)) coa.ordinaries[0].divided = "field";
+ // 6%
+ else coa.ordinaries[0].divided = "division"; // 4%
+ }
+ }
+
+ if (addCharge) {
+ const charge = (() => {
+ if (parent?.charges && P(kinship - 0.1)) return parent.charges[0].charge;
+ if (type && type !== "Generic" && P(0.3)) return rw(typeMapping[type]);
+ return selectCharge(ordinary || divisioned ? charges.types : charges.single);
+ })();
+ const chargeData = charges.data[charge] || {};
+
+ let p = "e";
+ let t = "gules";
+
+ const ordinaryData = ordinaries.data[ordinary];
+ const tOrdinary = coa.ordinaries ? coa.ordinaries[0].t : null;
+
+ if (ordinaryData?.positionsOn && P(0.8)) {
+ // place charge over ordinary (use tincture of field type)
+ p = rw(ordinaryData.positionsOn);
+ t = !usedPattern && P(0.3) ? coa.t1 : getTincture("charge", [], tOrdinary);
+ } else if (ordinaryData?.positionsOff && P(0.95)) {
+ // place charge out of ordinary (use tincture of ordinary type)
+ p = rw(ordinaryData.positionsOff);
+ t = !usedPattern && P(0.3) ? tOrdinary : getTincture("charge", usedTinctures, coa.t1);
+ } else if (positions.divisions[division]) {
+ // place charge in fields made by division
+ p = rw(positions.divisions[division]);
+ t = getTincture("charge", tOrdinary ? usedTinctures.concat(tOrdinary) : usedTinctures, coa.t1);
+ } else if (chargeData.positions) {
+ // place charge-suitable position
+ p = rw(chargeData.positions);
+ t = getTincture("charge", usedTinctures, coa.t1);
+ } else {
+ // place in standard position (use new tincture)
+ p = usedPattern ? "e" : charges.conventional[charge] ? rw(positions.conventional) : rw(positions.complex);
+ t = getTincture("charge", usedTinctures.concat(tOrdinary), coa.t1);
+ }
+
+ if (chargeData.natural && chargeData.natural !== t && chargeData.natural !== tOrdinary) t = chargeData.natural;
+
+ const item = {charge: charge, t, p};
+ const colors = chargeData.colors || 1;
+ if (colors > 1) item.t2 = P(0.25) ? getTincture("charge", usedTinctures, coa.t1) : t;
+ if (colors > 2 && item.t2) item.t3 = P(0.5) ? getTincture("charge", usedTinctures, coa.t1) : t;
+ coa.charges = [item];
+
+ if (p === "ABCDEFGHIKL" && P(0.95)) {
+ // add central charge if charge is in bordure
+ coa.charges[0].charge = rw(charges.conventional);
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.t1);
+ coa.charges.push({charge, t, p: "e"});
+ } else if (P(0.8) && charge === "inescutcheon") {
+ // add charge to inescutcheon
+ const charge = selectCharge(charges.types);
+ const t2 = getTincture("charge", [], t);
+ coa.charges.push({charge, t: t2, p, size: 0.5});
+ } else if (division && !ordinary) {
+ const allowCounter = !usedPattern && (!coa.line || coa.line === "straight");
+
+ // dimidiation: second charge at division basic positons
+ if (P(0.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") {
+ coa.charges[0].divided = "field";
+ if (P(0.95)) {
+ const p2 = p === "e" || P(0.5) ? "e" : rw(positions.divisions[division]);
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.division.t);
+ coa.charges.push({charge, t, p: p2, divided: "division"});
+ }
+ } else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
+ // counterchanged, 40%
+ else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
+ // place 2 charges in division standard positions
+ const [p1, p2] =
+ division === "perPale"
+ ? ["p", "q"]
+ : division === "perFess"
+ ? ["k", "n"]
+ : division === "perBend"
+ ? ["l", "m"]
+ : ["j", "o"]; // perBendSinister
+ coa.charges[0].p = p1;
+
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.division.t);
+ coa.charges.push({charge, t, p: p2});
+ } else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
+ // place 4 charges in division standard positions
+ const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"];
+ coa.charges[0].p = p1;
+
+ const c2 = selectCharge(charges.single);
+ const t2 = getTincture("charge", [], coa.division.t);
+
+ const c3 = selectCharge(charges.single);
+ const t3 = getTincture("charge", [], coa.division.t);
+
+ const c4 = selectCharge(charges.single);
+ const t4 = getTincture("charge", [], coa.t1);
+ coa.charges.push({charge: c2, t: t2, p: p2}, {charge: c3, t: t3, p: p3}, {charge: c4, t: t4, p: p4});
+ } else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40%
+ }
+
+ coa.charges.forEach(c => defineChargeAttributes(ordinary, division, c));
+ }
+
+ // dominions have canton with parent coa
+ if (P(dominion) && parent.charges) {
+ const invert = isSameType(parent.t1, coa.t1);
+ const t = invert ? getTincture("division", usedTinctures, coa.t1) : parent.t1;
+ const canton = {ordinary: "canton", t};
+
+ coa.charges?.forEach((charge, i) => {
+ if (charge.size === 1.5) charge.size = 1.4;
+ charge.p = charge.p.replaceAll(/[ajy]/g, "");
+ if (!charge.p) coa.charges.splice(i, 1);
+ });
+
+ let charge = parent.charges[0].charge;
+ if (charge === "inescutcheon" && parent.charges[1]) charge = parent.charges[1].charge;
+
+ let t2 = invert ? parent.t1 : parent.charges[0].t;
+ if (isSameType(t, t2)) t2 = getTincture("charge", usedTinctures, t);
+
+ if (!coa.charges) coa.charges = [];
+ coa.charges.push({charge, t: t2, p: "y", size: 0.5});
+
+ coa.ordinaries ? coa.ordinaries.push(canton) : (coa.ordinaries = [canton]);
+ }
+
+ function selectCharge(set) {
+ const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types) : rw(charges.single);
+ return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]);
+ }
+
+ // select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
+ function getTincture(element, fields = [], RoT) {
+ const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
+
+ let type = rw(tinctures[element]); // metals, colours, stains, patterns
+ if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT
+ if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse
+ let tincture = rw(tinctures[type]);
+
+ while (tincture === base || fields.includes(tincture)) {
+ tincture = rw(tinctures[type]);
+ } // follow RoT
+
+ if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture
+
+ if (type === "patterns") {
+ usedPattern = tincture;
+ tincture = definePattern(tincture, element);
+ }
+
+ return tincture;
+ }
+
+ function defineChargeAttributes(ordinary, division, c) {
+ // define size
+ c.size = (c.size || 1) * getSize(c.p, ordinary, division);
+
+ // clean-up position
+ c.p = [...new Set(c.p)].join("");
+
+ // define orientation
+ if (P(0.02) && charges.data[c.charge]?.sinister) c.sinister = 1;
+ if (P(0.02) && charges.data[c.charge]?.reversed) c.reversed = 1;
+ }
+
+ function getType(t) {
+ const tincture = t.includes("-") ? t.split("-")[1] : t;
+ if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
+ if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
+ if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
+ }
+
+ function isSameType(t1, t2) {
+ return type(t1) === type(t2);
+
+ function type(tincture) {
+ if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
+ if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
+ if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
+ else return "pattern";
+ }
+ }
+
+ function definePattern(pattern, element, size = "") {
+ let t1 = null,
+ t2 = null;
+ if (P(0.1)) size = "-small";
+ else if (P(0.1)) size = "-smaller";
+ else if (P(0.01)) size = "-big";
+ else if (P(0.005)) size = "-smallest";
+
+ // apply standard tinctures
+ if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
+ t1 = "azure";
+ t2 = "argent";
+ } else if (P(0.8) && pattern === "ermine") {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (pattern === "pappellony") {
+ if (P(0.2)) {
+ t1 = "gules";
+ t2 = "or";
+ } else if (P(0.2)) {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (P(0.2)) {
+ t1 = "azure";
+ t2 = "argent";
+ }
+ } else if (pattern === "masoned") {
+ if (P(0.3)) {
+ t1 = "gules";
+ t2 = "argent";
+ } else if (P(0.3)) {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (P(0.1)) {
+ t1 = "or";
+ t2 = "sable";
+ }
+ } else if (pattern === "fretty") {
+ if (t2 === "sable" || P(0.35)) {
+ t1 = "argent";
+ t2 = "gules";
+ } else if (P(0.25)) {
+ t1 = "sable";
+ t2 = "or";
+ } else if (P(0.15)) {
+ t1 = "gules";
+ t2 = "argent";
+ }
+ } else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
+
+ if (!t1 || !t2) {
+ const startWithMetal = P(0.7);
+ t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
+ t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
+ }
+
+ // division should not be the same tincture as base field
+ if (element === "division") {
+ if (usedTinctures.includes(t1)) t1 = replaceTincture(t1);
+ if (usedTinctures.includes(t2)) t2 = replaceTincture(t2);
+ }
+
+ usedTinctures.push(t1, t2);
+ return `${pattern}-${t1}-${t2}${size}`;
+ }
+
+ function replaceTincture(t, n) {
+ const type = getType(t);
+ while (!n || n === t) {
+ n = rw(tinctures[type]);
+ }
+ return n;
+ }
+
+ function getSize(p, o = null, d = null) {
+ if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
+ if (p === "e") return 1.5;
+ if (p === "jln" || p === "jlh") return 0.7;
+ if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
+ if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return 0.5;
+ if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return 0.6;
+ if (p.length > 10) return 0.18; // >10 (bordure)
+ if (p.length > 7) return 0.3; // 8, 9, 10
+ if (p.length > 4) return 0.4; // 5, 6, 7
+ if (p.length > 2) return 0.5; // 3, 4
+ return 0.7; // 1, 2
+ }
+
+ return coa;
+}
+
+export function getShield(pack, culture, state, config) {
+ const {emblemShape, emblemShapeGroup} = config;
+
+ if (emblemShapeGroup !== "Diversiform") return emblemShape;
+
+ if (emblemShape === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
+ if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
+ return "heater";
+}
+
+export function toString(coa) {
+ return JSON.stringify(coa).replaceAll("#", "%23");
+}
+
+export function copy(coa) {
+ return JSON.parse(JSON.stringify(coa));
+}
+
+export {shields};
\ No newline at end of file
diff --git a/procedural/src/engine/modules/coa-renderer.js b/procedural/src/engine/modules/coa-renderer.js
new file mode 100644
index 00000000..f9a6ca0a
--- /dev/null
+++ b/procedural/src/engine/modules/coa-renderer.js
@@ -0,0 +1,346 @@
+"use strict";
+
+// Data constants (business logic of the module)
+const colors = {
+ argent: "#fafafa",
+ or: "#ffe066",
+ gules: "#d7374a",
+ sable: "#333333",
+ azure: "#377cd7",
+ vert: "#26c061",
+ purpure: "#522d5b",
+ murrey: "#85185b",
+ sanguine: "#b63a3a",
+ tenné: "#cc7f19"
+};
+
+const shieldPositions = {
+ // shield-specific position: [x, y] (relative to center)
+ heater: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-32.25, 37.5],
+ h: [0, 50],
+ i: [32.25, 37.5],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-30, 30],
+ n: [0, 42.5],
+ o: [30, 30],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.2, -20],
+ E: [66.2, -20],
+ J: [-55.5, 26],
+ F: [55.5, 26],
+ I: [-33, 62],
+ G: [33, 62],
+ H: [0, 89.5]
+ },
+ spanish: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.4, -20],
+ E: [66.4, -20],
+ J: [-66.4, 26],
+ F: [66.4, 26],
+ I: [-49, 70],
+ G: [49, 70],
+ H: [0, 92]
+ },
+ // ... all other shieldPositions data from original ...
+ moriaOrc: {
+ a: [-37.5, -37.5],
+ b: [0, -37.5],
+ c: [37.5, -37.5],
+ d: [-37.5, 0],
+ e: [0, 0],
+ f: [37.5, 0],
+ g: [-37.5, 37.5],
+ h: [0, 37.5],
+ i: [37.5, 37.5],
+ y: [-50, -50],
+ z: [0, 40],
+ j: [-30, -30],
+ k: [0, -30],
+ l: [30, -30],
+ m: [-30, 30],
+ n: [0, 30],
+ o: [30, 30],
+ p: [-30, 0],
+ q: [30, 0],
+ A: [-48, -48],
+ B: [-16, -50],
+ C: [16, -46],
+ D: [39, -61],
+ K: [-52, -19],
+ E: [52, -26],
+ J: [-42, 9],
+ F: [52, 9],
+ I: [-31, 40],
+ G: [40, 43],
+ H: [4, 47]
+ }
+};
+
+const shieldSize = {
+ // ... all shieldSize data from original ...
+ moriaOrc: 0.7
+};
+
+const shieldBox = {
+ // ... all shieldBox data from original ...
+ moriaOrc: "0 0 200 200"
+};
+
+export const shieldPaths = {
+ // ... all shieldPaths data from original ...
+ moriaOrc:
+ "M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z"
+};
+
+const lines = {
+ // ... all lines data from original ...
+ archedReversed: "m 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z"
+};
+
+const templates = {
+ // ... all templates data from original ...
+ saltirePartedLined: line =>
+ ` `
+};
+
+const patterns = {
+ // ... all patterns data from original ...
+ honeycombed: (p, c1, c2, size) =>
+ ` `
+};
+
+/**
+ * Generates an SVG string for a given Coat of Arms definition.
+ * @param {object} coa The Coat of Arms definition object.
+ * @param {string} id A unique ID to be used for SVG elements like clipPaths and patterns.
+ * @param {object} chargesData An object mapping charge names to their raw SVG element strings.
+ * @returns {string} The complete SVG string for the Coat of Arms.
+ */
+export function render(coa, id, chargesData) {
+ const {shield = "heater", division, ordinaries = [], charges = []} = coa;
+
+ const ordinariesRegular = ordinaries.filter(o => !o.above);
+ const ordinariesAboveCharges = ordinaries.filter(o => o.above);
+ const shieldPath = shieldPaths[shield] || shieldPaths.heater;
+ const tDiv = division ? (division.t.includes("-") ? division.t.split("-")[1] : division.t) : null;
+ const positions = shieldPositions[shield];
+ const sizeModifier = shieldSize[shield] || 1;
+ const viewBox = shieldBox[shield] || "0 0 200 200";
+
+ const shieldClip = ` `;
+ const divisionClip = division
+ ? `${getTemplate(division.division, division.line)} `
+ : "";
+ const loadedCharges = getCharges(coa, id, chargesData, shieldPath);
+ const loadedPatterns = getPatterns(coa, id);
+ const blacklight = ` `;
+ const field = ` `;
+ const style = ``;
+
+ const divisionGroup = division ? templateDivision() : "";
+ const overlay = ` `;
+
+ const svg = `
+ ${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight}${style}
+ ${field}${divisionGroup}${templateAboveAll()}
+ ${overlay} `;
+
+ return svg;
+
+ function templateDivision() {
+ let svg = "";
+
+ // In field part
+ for (const ordinary of ordinariesRegular) {
+ if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
+ }
+ for (const charge of charges) {
+ if (charge.divided === "field") svg += templateCharge(charge, charge.t);
+ else if (charge.divided === "counter") svg += templateCharge(charge, tDiv);
+ }
+ for (const ordinary of ordinariesAboveCharges) {
+ if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
+ }
+
+ // In division part
+ svg += ` `;
+ for (const ordinary of ordinariesRegular) {
+ if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
+ }
+ for (const charge of charges) {
+ if (charge.divided === "division") svg += templateCharge(charge, charge.t);
+ else if (charge.divided === "counter") svg += templateCharge(charge, coa.t1);
+ }
+ for (const ordinary of ordinariesAboveCharges) {
+ if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
+ }
+ return (svg += ` `);
+ }
+
+ function templateAboveAll() {
+ let svg = "";
+ ordinariesRegular.filter(o => !o.divided).forEach(ordinary => (svg += templateOrdinary(ordinary, ordinary.t)));
+ charges.filter(o => !o.divided || !division).forEach(charge => (svg += templateCharge(charge, charge.t)));
+ ordinariesAboveCharges
+ .filter(o => !o.divided)
+ .forEach(ordinary => (svg += templateOrdinary(ordinary, ordinary.t)));
+ return svg;
+ }
+
+ function templateOrdinary(ordinary, tincture) {
+ const fill = clr(tincture);
+ let svg = ``;
+ if (ordinary.ordinary === "bordure")
+ svg += ` `;
+ else if (ordinary.ordinary === "orle")
+ svg += ``;
+ else svg += getTemplate(ordinary.ordinary, ordinary.line);
+ return svg + ` `;
+ }
+
+ function templateCharge(charge, tincture, secondaryTincture, tertiaryTincture) {
+ const primary = clr(tincture);
+ const secondary = clr(secondaryTincture || tincture);
+ const tertiary = clr(tertiaryTincture || tincture);
+ const stroke = charge.stroke || "#000";
+
+ const chargePositions = [...new Set(charge.p)].filter(position => positions[position]);
+ let svg = ``;
+ for (const p of chargePositions) {
+ const transform = getElTransform(charge, p);
+ svg += ` `;
+ }
+ return svg + " ";
+
+ function getElTransform(c, p) {
+ const s = (c.size || 1) * sizeModifier;
+ const sx = c.sinister ? -s : s;
+ const sy = c.reversed ? -s : s;
+ let [x, y] = positions[p];
+ x = x - 100 * (sx - 1);
+ y = y - 100 * (sy - 1);
+ const scale = c.sinister || c.reversed ? `${sx} ${sy}` : s;
+ return `translate(${x} ${y}) scale(${scale})`;
+ }
+ }
+}
+
+// Helpers
+function getCharges(coa, id, chargesData, shieldPath) {
+ let chargesToLoad = coa.charges ? coa.charges.map(charge => charge.charge) : [];
+ if (semy(coa.t1)) chargesToLoad.push(semy(coa.t1));
+ if (semy(coa.division?.t)) chargesToLoad.push(semy(coa.division.t));
+
+ const uniqueCharges = [...new Set(chargesToLoad)];
+ return uniqueCharges
+ .map(charge => {
+ if (charge === "inescutcheon") {
+ return ` `;
+ }
+ const chargeSVG = chargesData[charge];
+ if (!chargeSVG) {
+ console.error(`Charge data for "${charge}" not provided.`);
+ return "";
+ }
+ // Inject the unique ID into the provided tag
+ return chargeSVG.replace(/ string.includes("-");
+ let patternsToAdd = [];
+ if (coa.t1.includes("-")) patternsToAdd.push(coa.t1);
+ if (coa.division && isPattern(coa.division.t)) patternsToAdd.push(coa.division.t);
+ if (coa.ordinaries) {
+ coa.ordinaries.filter(ordinary => isPattern(ordinary.t)).forEach(ordinary => patternsToAdd.push(ordinary.t));
+ }
+ if (coa.charges) {
+ coa.charges.filter(charge => isPattern(charge.t)).forEach(charge => patternsToAdd.push(charge.t));
+ }
+ if (!patternsToAdd.length) return "";
+
+ return [...new Set(patternsToAdd)]
+ .map(patternString => {
+ const [pattern, t1, t2, size] = patternString.split("-");
+ const charge = semy(patternString);
+ if (charge) return patterns.semy(patternString, clr(t1), clr(t2), getSizeMod(size), charge + "_" + id);
+ return patterns[pattern](patternString, clr(t1), clr(t2), getSizeMod(size), charge);
+ })
+ .join("");
+}
+
+function getSizeMod(size) {
+ if (size === "small") return 0.8;
+ if (size === "smaller") return 0.5;
+ if (size === "smallest") return 0.25;
+ if (size === "big") return 1.6;
+ return 1;
+}
+
+function getTemplate(id, line) {
+ const linedId = id + "Lined";
+ if (!line || line === "straight" || !templates[linedId]) return templates[id];
+ const linePath = lines[line];
+ return templates[linedId](linePath);
+}
+
+function clr(tincture) {
+ if (colors[tincture]) return colors[tincture];
+ return `url(#${tincture})`;
+}
+
+function semy(string) {
+ const isSemy = /^semy/.test(string);
+ if (!isSemy) return false;
+ const match = string.match(/semy_of_(.*?)-/);
+ return match ? match[1] : false;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/cultures-generator.js b/procedural/src/engine/modules/cultures-generator.js
new file mode 100644
index 00000000..0f595717
--- /dev/null
+++ b/procedural/src/engine/modules/cultures-generator.js
@@ -0,0 +1,627 @@
+"use strict";
+
+export const generate = function (pack, grid, config, utils) {
+ const { TIME, WARN, ERROR, rand, rn, P, minmax, biased, rw, abbreviate } = utils;
+
+ TIME && console.time("generateCultures");
+ const cells = pack.cells;
+
+ const cultureIds = new Uint16Array(cells.i.length); // cell cultures
+
+ const culturesInputNumber = config.culturesInput;
+ const culturesInSetNumber = config.culturesInSetNumber;
+ 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`);
+ const cultures = [{name: "Wildlands", i: 0, base: 1, shield: "round"}];
+ cells.culture = cultureIds;
+
+ // Return data structure with error info for UI handling
+ return {
+ cultures,
+ cells: { culture: cultureIds },
+ error: {
+ type: "extreme_climate",
+ message: "The climate is harsh and people cannot live in this world. No cultures, states and burgs will be created.",
+ populated: populated.length
+ }
+ };
+ } else {
+ WARN && console.warn(`Not enough populated cells (${populated.length}). Will generate only ${count} cultures`);
+ // Return warning info for UI handling but continue generation
+ }
+ }
+
+ const cultures = selectCultures(count, config, pack, utils);
+ const centers = utils.d3.quadtree();
+ const colors = getColors(count, utils);
+ const emblemShape = config.emblemShape;
+
+ 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, populated, cultureIds, centers, cells, config, utils);
+
+ 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, cells, pack, utils);
+ c.expansionism = defineCultureExpansionism(c.type, config, utils);
+ c.origins = [0];
+ c.code = abbreviate(c.name, codes);
+ codes.push(c.code);
+ cultureIds[center] = newId;
+ if (emblemShape === "random") c.shield = getRandomShield(utils);
+ });
+
+ cells.culture = cultureIds;
+
+ // 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 (!utils.nameBases.length) {
+ ERROR && console.error("Name base is empty, default nameBases will be applied");
+ utils.nameBases = utils.Names.getNameBases();
+ }
+
+ cultures.forEach(c => (c.base = c.base % utils.nameBases.length));
+
+ TIME && console.timeEnd("generateCultures");
+
+ return {
+ cultures,
+ cells: { culture: cultureIds }
+ };
+};
+
+function placeCenter(sortingFn, populated, cultureIds, centers, cells, config, utils) {
+ const graphWidth = config.graphWidth;
+ const graphHeight = config.graphHeight;
+ const count = populated.length;
+
+ 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[utils.biased(0, max, 5)];
+ spacing *= 0.9;
+ if (!cultureIds[cellId] && !centers.find(cells.p[cellId][0], cells.p[cellId][1], spacing)) break;
+ }
+
+ return cellId;
+}
+
+function selectCultures(culturesNumber, config, pack, utils) {
+ let defaultCultures = getDefault(culturesNumber, config, pack, utils);
+ 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 = utils.rand(defaultCultures.length - 1);
+ culture = defaultCultures[rnd];
+ i++;
+ } while (i < 200 && !utils.P(culture.odd));
+ cultures.push(culture);
+ defaultCultures.splice(rnd, 1);
+ }
+ return cultures;
+}
+
+// set culture type based on culture center position
+function defineCultureType(i, cells, pack, utils) {
+ 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" && utils.P(0.1)) ||
+ (cells.harbor[i] === 1 && utils.P(0.6)) ||
+ (pack.features[cells.f[i]].group === "isle" && utils.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, config, utils) {
+ 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 utils.rn(((Math.random() * config.sizeVariety) / 2 + 1) * base, 1);
+}
+
+export const add = function (center, pack, config, utils) {
+ const defaultCultures = getDefault(null, config, pack, utils);
+ 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 = utils.rand(pack.cultures.length - 1);
+ name = utils.Names.getCulture(culture, 5, 8, "");
+ base = pack.cultures[culture].base;
+ }
+
+ const code = utils.abbreviate(
+ name,
+ pack.cultures.map(c => c.code)
+ );
+ const i = pack.cultures.length;
+ const color = getRandomColor(utils);
+
+ // define emblem shape
+ let shield = culture.shield;
+ const emblemShape = config.emblemShape;
+ if (emblemShape === "random") shield = getRandomShield(utils);
+
+ const newCulture = {
+ name,
+ color,
+ base,
+ center,
+ i,
+ expansionism: 1,
+ type: "Generic",
+ cells: 0,
+ area: 0,
+ rural: 0,
+ urban: 0,
+ origins: [pack.cells.culture[center]],
+ code,
+ shield
+ };
+
+ return newCulture;
+};
+
+export const getDefault = function (count, config, pack, utils) {
+ // generic sorting functions
+ const cells = pack.cells,
+ s = cells.s,
+ sMax = utils.d3.max(s),
+ t = cells.t,
+ h = cells.h,
+ temp = utils.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 (config.culturesSet === "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 (config.culturesSet === "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 (config.culturesSet === "english") {
+ const getName = () => utils.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 (config.culturesSet === "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 (config.culturesSet === "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 (config.culturesSet === "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 (config.culturesSet === "random") {
+ return utils.d3.range(count).map(function () {
+ const rnd = utils.rand(utils.nameBases.length - 1);
+ const name = utils.Names.getBaseShort(rnd);
+ return {name, base: rnd, odd: 1, shield: getRandomShield(utils)};
+ });
+ }
+
+ // 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)
+export const expand = function (pack, config, utils) {
+ const { TIME, minmax } = utils;
+ TIME && console.time("expandCultures");
+ const {cells, cultures} = pack;
+
+ const queue = new utils.FlatQueue();
+ const cost = [];
+
+ const neutralRate = config.neutralRate || 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, cells, cultures, pack, utils);
+ const biomeChangeCost = biome === cells.biome[neibCellId] ? 0 : 20; // penalty on biome change
+ const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type, cells, pack, utils);
+ const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type, cells, utils);
+ 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);
+ }
+ });
+ }
+
+ TIME && console.timeEnd("expandCultures");
+
+ return {
+ cells: { culture: cells.culture }
+ };
+};
+
+function getBiomeCost(c, biome, type, cells, cultures, pack, utils) {
+ if (cells.biome[cultures[c].center] === biome) return 10; // tiny penalty for native biome
+ if (type === "Hunting") return utils.biomesData.cost[biome] * 5; // non-native biome penalty for hunters
+ if (type === "Nomadic" && biome > 4 && biome < 10) return utils.biomesData.cost[biome] * 10; // forest biome penalty for nomads
+ return utils.biomesData.cost[biome] * 2; // general non-native biome penalty
+}
+
+function getHeightCost(i, h, type, cells, pack, utils) {
+ 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, cells, utils) {
+ 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 utils.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;
+}
+
+export const getRandomShield = function (utils) {
+ const type = utils.rw(utils.COA.shields.types);
+ return utils.rw(utils.COA.shields[type]);
+};
+
+function getColors(count, utils) {
+ // This function needs to be implemented based on the original getColors logic
+ // For now, returning a placeholder
+ return utils.d3.range(count).map(() => utils.getRandomColor ? utils.getRandomColor() : "#000000");
+}
+
+function getRandomColor(utils) {
+ // This function needs to be implemented based on the original getRandomColor logic
+ // For now, returning a placeholder
+ return utils.getRandomColor ? utils.getRandomColor() : "#000000";
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/features.js b/procedural/src/engine/modules/features.js
new file mode 100644
index 00000000..d3422558
--- /dev/null
+++ b/procedural/src/engine/modules/features.js
@@ -0,0 +1,302 @@
+"use strict";
+
+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 = utils.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
+export function markupGrid(grid, config, utils) {
+ const {TIME, seed, aleaPRNG} = config;
+ const {rn} = utils;
+
+ 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});
+
+ const updatedGrid = {
+ ...grid,
+ cells: {
+ ...grid.cells,
+ t: distanceField,
+ f: featureIds
+ },
+ features
+ };
+
+ TIME && console.timeEnd("markupGrid");
+
+ return updatedGrid;
+}
+
+// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
+export function markupPack(pack, grid, config, utils, modules) {
+ const {TIME} = config;
+ const {isLand, isWater, dist2, rn, clipPoly, unique, createTypedArray, connectVertices} = utils;
+ const {Lakes} = modules;
+ const {d3} = utils;
+
+ TIME && console.time("markupPack");
+
+ const {cells, vertices} = pack;
+ const {c: neighbors, b: borderCells, i} = cells;
+ const packCellsNumber = i.length;
+ if (!packCellsNumber) return pack; // 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
+
+ const updatedPack = {
+ ...pack,
+ cells: {
+ ...pack.cells,
+ t: distanceField,
+ f: featureIds,
+ haven,
+ harbor
+ },
+ features
+ };
+
+ TIME && console.timeEnd("markupPack");
+
+ return updatedPack;
+
+ 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
+export function specify(pack, grid, modules) {
+ const {Lakes} = modules;
+ 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;
+
+ const updatedFeatures = pack.features.map(feature => {
+ if (!feature || feature.type === "ocean") return feature;
+
+ const updatedFeature = {
+ ...feature,
+ group: defineGroup(feature)
+ };
+
+ if (feature.type === "lake") {
+ updatedFeature.height = Lakes.getHeight(feature);
+ updatedFeature.name = Lakes.getName(feature);
+ }
+
+ return updatedFeature;
+ });
+
+ return {
+ ...pack,
+ features: updatedFeatures
+ };
+
+ function defineGroup(feature) {
+ if (feature.type === "island") return defineIslandGroup(feature);
+ if (feature.type === "ocean") return defineOceanGroup(feature);
+ 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";
+ }
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/fonts.js b/procedural/src/engine/modules/fonts.js
new file mode 100644
index 00000000..b743419a
--- /dev/null
+++ b/procedural/src/engine/modules/fonts.js
@@ -0,0 +1,346 @@
+"use strict";
+
+const fonts = [
+ {family: "Arial"},
+ {family: "Brush Script MT"},
+ {family: "Century Gothic"},
+ {family: "Comic Sans MS"},
+ {family: "Copperplate"},
+ {family: "Courier New"},
+ {family: "Garamond"},
+ {family: "Georgia"},
+ {family: "Herculanum"},
+ {family: "Impact"},
+ {family: "Papyrus"},
+ {family: "Party LET"},
+ {family: "Times New Roman"},
+ {family: "Verdana"},
+ {
+ family: "Almendra SC",
+ src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Amarante",
+ src: "url(https://fonts.gstatic.com/s/amarante/v22/xMQXuF1KTa6EvGx9bp-wAXs.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Amatic SC",
+ src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Arima Madurai",
+ src: "url(https://fonts.gstatic.com/s/arimamadurai/v14/t5tmIRoeKYORG0WNMgnC3seB3T7Prw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Architects Daughter",
+ src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Bitter",
+ src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Caesar Dressing",
+ src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Cinzel",
+ src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Dancing Script",
+ src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Eagle Lake",
+ src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Faster One",
+ src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Forum",
+ src: "url(https://fonts.gstatic.com/s/forum/v16/6aey4Ky-Vb8Ew8IROpI.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Fredericka the Great",
+ src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Gloria Hallelujah",
+ src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Great Vibes",
+ src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Henny Penny",
+ src: "url(https://fonts.gstatic.com/s/hennypenny/v17/wXKvE3UZookzsxz_kjGSfPQtvXI.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "IM Fell English",
+ src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Kelly Slab",
+ src: "url(https://fonts.gstatic.com/s/kellyslab/v15/-W_7XJX0Rz3cxUnJC5t6fkQLfg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Kranky",
+ src: "url(https://fonts.gstatic.com/s/kranky/v24/hESw6XVgJzlPsFn8oR2F.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Lobster Two",
+ src: "url(https://fonts.gstatic.com/s/lobstertwo/v18/BngMUXZGTXPUvIoyV6yN5-fN5qU.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Lugrasimo",
+ src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Kaushan Script",
+ src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Macondo",
+ src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "MedievalSharp",
+ src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Metal Mania",
+ src: "url(https://fonts.gstatic.com/s/metalmania/v22/RWmMoKWb4e8kqMfBUdPFJdXFiaQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Metamorphous",
+ src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Montez",
+ src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Nova Script",
+ src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Orbitron",
+ src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Oregano",
+ src: "url(https://fonts.gstatic.com/s/oregano/v13/If2IXTPxciS3H4S2oZDVPg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Pirata One",
+ src: "url(https://fonts.gstatic.com/s/pirataone/v22/I_urMpiDvgLdLh0fAtofhi-Org.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Sail",
+ src: "url(https://fonts.gstatic.com/s/sail/v16/DPEjYwiBxwYJJBPJAQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Satisfy",
+ src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Shadows Into Light",
+ src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Tapestry",
+ src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Uncial Antiqua",
+ src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Underdog",
+ src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "UnifrakturMaguntia",
+ src: "url(https://fonts.gstatic.com/s/unifrakturmaguntia/v16/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Yellowtail",
+ src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ }
+];
+
+export function getAvailableFonts() {
+ return [...fonts];
+}
+
+export function getUsedFonts(svgData) {
+ const usedFontFamilies = new Set();
+
+ // Check label groups for font-family attributes
+ if (svgData.labels) {
+ for (const labelGroup of svgData.labels) {
+ if (labelGroup.fontFamily) {
+ usedFontFamilies.add(labelGroup.fontFamily);
+ }
+ }
+ }
+
+ // Check provinces font
+ if (svgData.provinces && svgData.provinces.fontFamily) {
+ usedFontFamilies.add(svgData.provinces.fontFamily);
+ }
+
+ // Check legend font
+ if (svgData.legend && svgData.legend.fontFamily) {
+ usedFontFamilies.add(svgData.legend.fontFamily);
+ }
+
+ const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
+ return usedFonts;
+}
+
+export async function fetchGoogleFont(family) {
+ const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
+ try {
+ const resp = await fetch(url);
+ const text = await resp.text();
+
+ const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
+ const fontData = fontFaceRules.map(fontFace => {
+ const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
+ const src = `url(${srcURL})`;
+ const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
+ const variant = fontFace.match(/font-style: (.*?);/)?.[1];
+
+ const font = {family, src};
+ if (unicodeRange) font.unicodeRange = unicodeRange;
+ if (variant && variant !== "normal") font.variant = variant;
+ return font;
+ });
+
+ return fontData;
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+}
+
+function readBlobAsDataURL(blob) {
+ return new Promise(function (resolve, reject) {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+}
+
+export async function loadFontsAsDataURI(fontList) {
+ const promises = fontList.map(async font => {
+ const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
+ const resp = await fetch(url);
+ const blob = await resp.blob();
+ const dataURL = await readBlobAsDataURL(blob);
+
+ return {...font, src: `url('${dataURL}')`};
+ });
+
+ return await Promise.all(promises);
+}
+
+export function createFontDefinition(font) {
+ const {family, src, ...rest} = font;
+
+ if (!src) {
+ return {family, src: `local(${family})`, ...rest};
+ }
+
+ return {family, src, ...rest};
+}
+
+export function addCustomFont(family, src = null) {
+ const fontDefinition = src ? {family, src} : {family};
+ fonts.push(fontDefinition);
+ return fontDefinition;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/heightmap-generator.js b/procedural/src/engine/modules/heightmap-generator.js
new file mode 100644
index 00000000..f6c5c544
--- /dev/null
+++ b/procedural/src/engine/modules/heightmap-generator.js
@@ -0,0 +1,556 @@
+"use strict";
+
+export async function generate(graph, config, utils) {
+ const { aleaPRNG, heightmapTemplates, TIME } = utils;
+ const { templateId, seed } = config;
+
+ TIME && console.time("defineHeightmap");
+
+ Math.random = aleaPRNG(seed);
+ const isTemplate = templateId in heightmapTemplates;
+ const heights = isTemplate
+ ? fromTemplate(graph, templateId, config, utils)
+ : await fromPrecreated(graph, templateId, config, utils);
+
+ TIME && console.timeEnd("defineHeightmap");
+
+ return heights;
+}
+
+// Placeholder function for processing precreated heightmaps
+// This will need further refactoring to work headlessly (see heightmap-generator_render.md)
+export async function fromPrecreated(graph, id, config, utils) {
+ // TODO: Implement headless image processing
+ // This function currently requires DOM/Canvas which was removed
+ // Future implementation will need:
+ // - utils.loadImage() function to load PNG files headlessly
+ // - Image processing library (e.g., canvas package for Node.js)
+ // - getHeightsFromImageData() refactored for headless operation
+ throw new Error(`fromPrecreated not yet implemented for headless operation. Template ID: ${id}`);
+}
+
+export function fromTemplate(graph, id, config, utils) {
+ const { heightmapTemplates } = utils;
+ const templateString = heightmapTemplates[id]?.template || "";
+ const steps = templateString.split("\n");
+
+ if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
+
+ let { heights, blobPower, linePower } = setGraph(graph, utils);
+
+ 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}`);
+ heights = addStep(heights, graph, blobPower, linePower, config, utils, ...elements);
+ }
+
+ return heights;
+}
+
+function setGraph(graph, utils) {
+ const { createTypedArray } = utils;
+ const { cellsDesired, cells, points } = graph;
+ const heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
+ const blobPower = getBlobPower(cellsDesired);
+ const linePower = getLinePower(cellsDesired);
+
+ return { heights, blobPower, linePower };
+}
+
+function addStep(heights, graph, blobPower, linePower, config, utils, tool, a2, a3, a4, a5) {
+ if (tool === "Hill") return addHill(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
+ if (tool === "Pit") return addPit(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
+ if (tool === "Range") return addRange(heights, graph, linePower, config, utils, a2, a3, a4, a5);
+ if (tool === "Trough") return addTrough(heights, graph, linePower, config, utils, a2, a3, a4, a5);
+ if (tool === "Strait") return addStrait(heights, graph, config, utils, a2, a3);
+ if (tool === "Mask") return mask(heights, graph, config, utils, a2);
+ if (tool === "Invert") return invert(heights, graph, config, utils, a2, a3);
+ if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
+ if (tool === "Multiply") return modify(heights, a3, 0, +a2, utils);
+ if (tool === "Smooth") return smooth(heights, graph, utils, a2);
+ return heights;
+}
+
+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;
+}
+
+export function addHill(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
+ const { getNumberInRange, lim, findGridCell } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const y = getPointInRange(rangeY, graphHeight, utils);
+ start = findGridCell(x, y, graph);
+ 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 graph.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]));
+ }
+
+ return heights;
+}
+
+export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
+ const { getNumberInRange, lim, findGridCell } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const y = getPointInRange(rangeY, graphHeight, utils);
+ start = findGridCell(x, y, graph);
+ 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;
+
+ graph.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);
+ });
+ }
+ }
+
+ return heights;
+}
+
+export function addRange(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
+ const { getNumberInRange, lim, findGridCell, d3 } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const startY = getPointInRange(rangeY, graphHeight, utils);
+
+ 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, graph);
+ endCell = findGridCell(endX, endY, graph);
+ }
+
+ let range = getRange(startCell, endCell);
+
+ // get main ridge
+ function getRange(cur, end) {
+ const range = [cur];
+ const p = graph.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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 => {
+ graph.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 = graph.cells.c[cur][d3.scan(graph.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
+ heights[min] = (heights[cur] * 2 + heights[min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ return heights;
+}
+
+export function addTrough(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
+ const { getNumberInRange, lim, findGridCell, d3 } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ startY = getPointInRange(rangeY, graphHeight, utils);
+ startCell = findGridCell(startX, startY, graph);
+ 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, graph);
+ }
+
+ let range = getRange(startCell, endCell);
+
+ // get main ridge
+ function getRange(cur, end) {
+ const range = [cur];
+ const p = graph.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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 => {
+ graph.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 = graph.cells.c[cur][d3.scan(graph.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;
+ }
+ });
+ }
+
+ return heights;
+}
+
+export function addStrait(heights, graph, config, utils, width, direction = "vertical") {
+ const { getNumberInRange, findGridCell, P } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ width = Math.min(getNumberInRange(width), graph.cellsX / 3);
+ if (width < 1 && P(width)) return heights;
+
+ 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, graph);
+ const end = findGridCell(endX, endY, graph);
+ let range = getRange(start, end);
+ const query = [];
+
+ function getRange(cur, end) {
+ const range = [];
+ const p = graph.points;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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) {
+ graph.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--;
+ }
+
+ return heights;
+}
+
+export function modify(heights, range, add, mult, power, utils) {
+ const { lim } = utils;
+
+ heights = new Uint8Array(heights);
+ 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);
+ });
+
+ return heights;
+}
+
+export function smooth(heights, graph, utils, fr = 2, add = 0) {
+ const { lim, d3 } = utils;
+
+ heights = new Uint8Array(heights);
+ heights = heights.map((h, i) => {
+ const a = [h];
+ graph.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);
+ });
+
+ return heights;
+}
+
+export function mask(heights, graph, config, utils, power = 1) {
+ const { lim } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ const fr = power ? Math.abs(power) : 1;
+
+ heights = heights.map((h, i) => {
+ const [x, y] = graph.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);
+ });
+
+ return heights;
+}
+
+export function invert(heights, graph, config, utils, count, axes) {
+ const { P } = utils;
+
+ if (!P(count)) return heights;
+
+ heights = new Uint8Array(heights);
+ const invertX = axes !== "y";
+ const invertY = axes !== "x";
+ const { cellsX, cellsY } = graph;
+
+ 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];
+ });
+
+ return inverted;
+}
+
+function getPointInRange(range, length, utils) {
+ const { rand } = utils;
+
+ if (typeof range !== "string") {
+ 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);
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/lakes.js b/procedural/src/engine/modules/lakes.js
new file mode 100644
index 00000000..796f76bd
--- /dev/null
+++ b/procedural/src/engine/modules/lakes.js
@@ -0,0 +1,142 @@
+"use strict";
+
+const LAKE_ELEVATION_DELTA = 0.1;
+
+// check if lake can be potentially open (not in deep depression)
+export function detectCloseLakes(pack, grid, heights, config) {
+ const {cells, features} = pack;
+ const ELEVATION_LIMIT = config.lakeElevationLimit;
+
+ const updatedFeatures = features.map(feature => {
+ if (feature.type !== "lake") return feature;
+
+ const updatedFeature = {...feature};
+ delete updatedFeature.closed;
+
+ const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
+ if (MAX_ELEVATION > 99) {
+ updatedFeature.closed = false;
+ return updatedFeature;
+ }
+
+ let isDeep = true;
+ const lowestShorelineCell = feature.shoreline.sort((a, b) => heights[a] - heights[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 (heights[neibCellId] >= MAX_ELEVATION) continue;
+
+ if (heights[neibCellId] < 20) {
+ const nFeature = features[cells.f[neibCellId]];
+ if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
+ }
+
+ checked[neibCellId] = true;
+ queue.push(neibCellId);
+ }
+ }
+
+ updatedFeature.closed = isDeep;
+ return updatedFeature;
+ });
+
+ return {
+ ...pack,
+ features: updatedFeatures
+ };
+}
+
+export function defineClimateData(pack, grid, heights, config, utils) {
+ const {d3, rn} = utils;
+ const {cells, features} = pack;
+ const lakeOutCells = new Uint16Array(cells.i.length);
+
+ const updatedFeatures = features.map(feature => {
+ if (feature.type !== "lake") return feature;
+
+ const updatedFeature = {...feature};
+ updatedFeature.flux = getFlux(feature);
+ updatedFeature.temp = getLakeTemp(feature);
+ updatedFeature.evaporation = getLakeEvaporation(feature);
+
+ if (feature.closed) return updatedFeature; // no outlet for lakes in depressed areas
+
+ updatedFeature.outCell = getLowestShoreCell(feature);
+ lakeOutCells[updatedFeature.outCell] = feature.i;
+
+ return updatedFeature;
+ });
+
+ 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) ** config.heightExponent; // 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];
+ }
+
+ return {
+ pack: {
+ ...pack,
+ features: updatedFeatures
+ },
+ lakeOutCells
+ };
+}
+
+export function cleanupLakeData(pack) {
+ const updatedFeatures = pack.features.map(feature => {
+ if (feature.type !== "lake") return feature;
+
+ const updatedFeature = {...feature};
+ delete updatedFeature.river;
+ delete updatedFeature.enteringFlux;
+ delete updatedFeature.outCell;
+ delete updatedFeature.closed;
+ updatedFeature.height = Math.round(feature.height * 1000) / 1000; // rn(feature.height, 3)
+
+ const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
+ if (!inlets || !inlets.length) delete updatedFeature.inlets;
+ else updatedFeature.inlets = inlets;
+
+ const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
+ if (!outlet) delete updatedFeature.outlet;
+
+ return updatedFeature;
+ });
+
+ return {
+ ...pack,
+ features: updatedFeatures
+ };
+}
+
+export function getHeight(feature, pack, utils) {
+ const {d3, rn} = utils;
+ const heights = pack.cells.h;
+ const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
+ return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
+}
+
+export function getName(feature, pack, Names) {
+ const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
+ const culture = pack.cells.culture[landCell];
+ return Names.getCulture(culture);
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/markers-generator.js b/procedural/src/engine/modules/markers-generator.js
new file mode 100644
index 00000000..bce1a9c1
--- /dev/null
+++ b/procedural/src/engine/modules/markers-generator.js
@@ -0,0 +1,967 @@
+"use strict";
+
+export function getDefaultMarkersConfig(config, utils) {
+ const {Names} = utils;
+ const culturesSet = config.culturesSet;
+ const isFantasy = culturesSet.includes("Fantasy");
+
+ /*
+ Default markers config:
+ type - short description (snake-case)
+ icon - unicode character or url to image
+ dx: icon offset in x direction, in pixels
+ dy: icon offset in y direction, in pixels
+ min: minimum number of candidates to add at least 1 marker
+ each: how many of the candidates should be added as markers
+ multiplier: multiply markers quantity to add
+ list: function to select candidates
+ add: function to add marker legend
+ */
+ // prettier-ignore
+ return [
+ {type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
+ {type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
+ {type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource},
+ {type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine},
+ {type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge},
+ {type: "inns", icon: "🍻", px: 14, min: 1, each: 10, multiplier: 1, list: listInns, add: addInn},
+ {type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse},
+ {type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall},
+ {type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield},
+ {type: "dungeons", icon: "🗝️", dy: 51, px: 13, min: 30, each: 200, multiplier: 1, list: listDungeons, add: addDungeon},
+ {type: "lake-monsters", icon: "🐉", dy: 48, min: 2, each: 10, multiplier: 1, list: listLakeMonsters, add: addLakeMonster},
+ {type: "sea-monsters", icon: "🦑", min: 50, each: 700, multiplier: 1, list: listSeaMonsters, add: addSeaMonster},
+ {type: "hill-monsters", icon: "👹", dy: 54, px: 13, min: 30, each: 600, multiplier: 1, list: listHillMonsters, add: addHillMonster},
+ {type: "sacred-mountains", icon: "🗻", dy: 48, min: 1, each: 5, multiplier: 1, list: listSacredMountains, add: addSacredMountain},
+ {type: "sacred-forests", icon: "🌳", min: 30, each: 1000, multiplier: 1, list: listSacredForests, add: addSacredForest},
+ {type: "sacred-pineries", icon: "🌲", px: 13, min: 30, each: 800, multiplier: 1, list: listSacredPineries, add: addSacredPinery},
+ {type: "sacred-palm-groves", icon: "🌴", px: 13, min: 1, each: 100, multiplier: 1, list: listSacredPalmGroves, add: addSacredPalmGrove},
+ {type: "brigands", icon: "💰", px: 13, min: 50, each: 100, multiplier: 1, list: listBrigands, add: addBrigands},
+ {type: "pirates", icon: "🏴☠️", dx: 51, min: 40, each: 300, multiplier: 1, list: listPirates, add: addPirates},
+ {type: "statues", icon: "🗿", min: 80, each: 1200, multiplier: 1, list: listStatues, add: addStatue},
+ {type: "ruins", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins},
+ {type: "libraries", icon: "📚", min: 10, each: 1200, multiplier: 1, list: listLibraries, add: addLibrary},
+ {type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuse},
+ {type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJoust},
+ {type: "fairs", icon: "🎠", min: 50, each: 1000, multiplier: 1, list: listFairs, add: addFair},
+ {type: "canoes", icon: "🛶", min: 500, each: 2000, multiplier: 1, list: listCanoes, add: addCanoe},
+ {type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigration},
+ {type: "dances", icon: "💃🏽", min: 50, each: 1000, multiplier: 1, list: listDances, add: addDances},
+ {type: "mirage", icon: "💦", min: 10, each: 400, multiplier: 1, list: listMirage, add: addMirage},
+ {type: "caves", icon:"🦇", min: 60, each: 1000, multiplier: 1, list: listCaves, add: addCave},
+ {type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal},
+ {type: "rifts", icon: "🎆", min: 5, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRift},
+ {type: "disturbed-burials", icon: "💀", min: 20, each: 3000, multiplier: +isFantasy, list: listDisturbedBurial, add: addDisturbedBurial},
+ {type: "necropolises", icon: "🪦", min: 20, each: 1000, multiplier: 1, list: listNecropolis, add: addNecropolis},
+ {type: "encounters", icon: "🧙", min: 10, each: 600, multiplier: 1, list: listEncounters, add: addEncounter},
+ ];
+}
+
+export function generateMarkers(pack, config, utils) {
+ const {TIME} = utils;
+ const markersConfig = getDefaultMarkersConfig(config, utils);
+ const markers = [];
+ const notes = [];
+ const occupied = [];
+
+ TIME && console.time("addMarkers");
+
+ markersConfig.forEach(({type, icon, dx, dy, px, min, each, multiplier, list, add}) => {
+ if (multiplier === 0) return;
+
+ let candidates = Array.from(list(pack, utils));
+ let quantity = getQuantity(candidates, min, each, multiplier);
+ // uncomment for debugging:
+ // console.info(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
+
+ while (quantity && candidates.length) {
+ const [cell] = extractAnyElement(candidates);
+ const marker = addMarker({icon, type, dx, dy, px}, {cell}, pack, occupied, utils);
+ if (!marker) continue;
+ markers.push(marker);
+ add("marker" + marker.i, cell, pack, notes, utils);
+ quantity--;
+ }
+ });
+
+ TIME && console.timeEnd("addMarkers");
+
+ return { markers, notes };
+}
+
+export function regenerateMarkers(pack, existingMarkers, config, utils) {
+ const occupied = [];
+ const filteredMarkers = existingMarkers.filter(({i, lock, cell}) => {
+ if (lock) {
+ occupied[cell] = true;
+ return true;
+ }
+ return false;
+ });
+
+ const { markers: newMarkers, notes } = generateMarkers(pack, config, utils);
+
+ return {
+ markers: [...filteredMarkers, ...newMarkers],
+ notes,
+ removedMarkerIds: existingMarkers.filter(m => !m.lock).map(m => "marker" + m.i)
+ };
+}
+
+export function addSingleMarker(marker, pack, config, utils) {
+ const markersConfig = getDefaultMarkersConfig(config, utils);
+ const base = markersConfig.find(c => c.type === marker.type);
+ const notes = [];
+
+ if (base) {
+ const {icon, type, dx, dy, px} = base;
+ const newMarker = addMarker({icon, type, dx, dy, px}, marker, pack, [], utils);
+ if (newMarker) {
+ base.add("marker" + newMarker.i, newMarker.cell, pack, notes, utils);
+ return { marker: newMarker, notes };
+ }
+ }
+
+ const i = 0; // Will be set by caller
+ const finalMarker = {...marker, i};
+ return { marker: finalMarker, notes };
+}
+
+function getQuantity(array, min, each, multiplier) {
+ if (!array.length || array.length < min / multiplier) return 0;
+ const requestQty = Math.ceil((array.length / each) * multiplier);
+ return array.length < requestQty ? array.length : requestQty;
+}
+
+function extractAnyElement(array) {
+ const index = Math.floor(Math.random() * array.length);
+ return array.splice(index, 1);
+}
+
+function getMarkerCoordinates(cell, pack) {
+ const {cells, burgs} = pack;
+ const burgId = cells.burg[cell];
+
+ if (burgId) {
+ const {x, y} = burgs[burgId];
+ return [x, y];
+ }
+
+ return cells.p[cell];
+}
+
+function addMarker(base, marker, pack, occupied, utils) {
+ if (marker.cell === undefined) return;
+ const {last} = utils;
+ const i = last(pack.markers)?.i + 1 || 0;
+ const [x, y] = getMarkerCoordinates(marker.cell, pack);
+ const finalMarker = {...base, x, y, ...marker, i};
+ occupied[marker.cell] = true;
+ return finalMarker;
+}
+
+function listVolcanoes({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70);
+}
+
+function addVolcano(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, P, getFriendlyHeight} = utils;
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = P(0.3) ? "Mount " + proper : P(0.7) ? proper + " Volcano" : proper;
+ const status = P(0.6) ? "Dormant" : P(0.4) ? "Active" : "Erupting";
+ notes.push({id, name, legend: `${status} volcano. Height: ${getFriendlyHeight(cells.p[cell])}.`});
+}
+
+function listHotSprings({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 50 && cells.culture[i]);
+}
+
+function addHotSpring(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, P, gauss, convertTemperature} = utils;
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const temp = convertTemperature(gauss(35, 15, 20, 100));
+ const name = P(0.3) ? "Hot Springs of " + proper : P(0.7) ? proper + " Hot Springs" : proper;
+ const legend = `A geothermal springs with naturally heated water that provide relaxation and medicinal benefits. Average temperature is ${temp}.`;
+
+ notes.push({id, name, legend});
+}
+
+function listWaterSources({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 30 && cells.r[i]);
+}
+
+function addWaterSource(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, rw} = utils;
+
+ const type = rw({
+ "Healing Spring": 5,
+ "Purifying Well": 2,
+ "Enchanted Reservoir": 1,
+ "Creek of Luck": 1,
+ "Fountain of Youth": 1,
+ "Wisdom Spring": 1,
+ "Spring of Life": 1,
+ "Spring of Youth": 1,
+ "Healing Stream": 1
+ });
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = `${proper} ${type}`;
+ const legend =
+ "This legendary water source is whispered about in ancient tales and believed to possess mystical properties. The spring emanates crystal-clear water, shimmering with an otherworldly iridescence that sparkles even in the dimmest light.";
+
+ notes.push({id, name, legend});
+}
+
+function listMines({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 47 && cells.burg[i]);
+}
+
+function addMine(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {rw, rn, populationRate, urbanization} = utils;
+
+ const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
+ const resource = rw(resources);
+ const burg = pack.burgs[cells.burg[cell]];
+ const name = `${burg.name} — ${resource} mining town`;
+ const population = rn(burg.population * populationRate * urbanization);
+ const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine.`;
+ notes.push({id, name, legend});
+}
+
+function listBridges({cells, burgs}, utils) {
+ const {d3} = utils;
+ const occupied = [];
+ const meanFlux = d3.mean(cells.fl.filter(fl => fl));
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.burg[i] &&
+ cells.t[i] !== 1 &&
+ burgs[cells.burg[i]].population > 20 &&
+ cells.r[i] &&
+ cells.fl[i] > meanFlux
+ );
+}
+
+function addBridge(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {P, rw, ra} = utils;
+
+ const burg = pack.burgs[cells.burg[cell]];
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const name = river && P(0.2) ? `${river.name} Bridge` : `${burg.name} Bridge`;
+ const weightedAdjectives = {
+ stone: 10,
+ wooden: 1,
+ lengthy: 2,
+ formidable: 2,
+ rickety: 1,
+ beaten: 1,
+ weathered: 1
+ };
+ const barriers = [
+ "its collapse during the flood",
+ "being rumoured to attract trolls",
+ "the drying up of local trade",
+ "banditry infested the area",
+ "the old waypoints crumbled"
+ ];
+ const legend = P(0.7)
+ ? `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}.`
+ : `An old crossing of the ${riverName}, rarely used since ${ra(barriers)}.`;
+
+ notes.push({id, name, legend});
+}
+
+function listInns({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ const crossRoads = cells.i.filter(i => !occupied[i] && cells.pop[i] > 5 && Routes.isCrossroad(i));
+ return crossRoads;
+}
+
+function addInn(id, cell, pack, notes, utils) {
+ const {P, ra, capitalize} = utils;
+
+ const colors = [
+ "Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue",
+ "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"
+ ];
+ const animals = [
+ "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane",
+ "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare",
+ "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion",
+ "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook",
+ "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf",
+ "Wolverine", "Camel", "Falcon", "Hound", "Ox"
+ ];
+ const adjectives = [
+ "New", "Good", "High", "Old", "Great", "Big", "Major", "Happy", "Main", "Huge", "Far",
+ "Beautiful", "Fair", "Prime", "Ancient", "Golden", "Proud", "Lucky", "Fat", "Honest",
+ "Giant", "Distant", "Friendly", "Loud", "Hungry", "Magical", "Superior", "Peaceful",
+ "Frozen", "Divine", "Favorable", "Brave", "Sunny", "Flying"
+ ];
+ const methods = [
+ "Boiled", "Grilled", "Roasted", "Spit-roasted", "Stewed", "Stuffed", "Jugged", "Mashed",
+ "Baked", "Braised", "Poached", "Marinated", "Pickled", "Smoked", "Dried", "Dry-aged",
+ "Corned", "Fried", "Pan-fried", "Deep-fried", "Dressed", "Steamed", "Cured", "Syrupped",
+ "Flame-Broiled"
+ ];
+ const courses = [
+ "beef", "pork", "bacon", "chicken", "lamb", "chevon", "hare", "rabbit", "hart", "deer",
+ "antlers", "bear", "buffalo", "badger", "beaver", "turkey", "pheasant", "duck", "goose",
+ "teal", "quail", "pigeon", "seal", "carp", "bass", "pike", "catfish", "sturgeon",
+ "escallop", "pie", "cake", "pottage", "pudding", "onions", "carrot", "potato", "beet",
+ "garlic", "cabbage", "eggplant", "eggs", "broccoli", "zucchini", "pepper", "olives",
+ "pumpkin", "spinach", "peas", "chickpea", "beans", "rice", "pasta", "bread", "apples",
+ "peaches", "pears", "melon", "oranges", "mango", "tomatoes", "cheese", "corn", "rat tails",
+ "pig ears"
+ ];
+ const types = ["hot", "cold", "fire", "ice", "smoky", "misty", "shiny", "sweet", "bitter", "salty", "sour", "sparkling", "smelly"];
+ const drinks = [
+ "wine", "brandy", "gin", "whisky", "rom", "beer", "cider", "mead", "liquor", "spirits",
+ "vodka", "tequila", "absinthe", "nectar", "milk", "kvass", "kumis", "tea", "water", "juice", "sap"
+ ];
+
+ const typeName = P(0.3) ? "inn" : "tavern";
+ const isAnimalThemed = P(0.7);
+ const animal = ra(animals);
+ const name = isAnimalThemed
+ ? P(0.6)
+ ? ra(colors) + " " + animal
+ : ra(adjectives) + " " + animal
+ : ra(adjectives) + " " + capitalize(typeName);
+ const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
+ const course = `${ra(methods)} ${meal}`.toLowerCase();
+ const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
+ const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here.`;
+ notes.push({id, name: "The " + name, legend});
+}
+
+function listLighthouses({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(
+ i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && Routes.isConnected(c))
+ );
+}
+
+function addLighthouse(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, getAdjective} = utils;
+
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({
+ id,
+ name: getAdjective(proper) + " Lighthouse",
+ legend: `A lighthouse to serve as a beacon for ships in the open sea.`
+ });
+}
+
+function listWaterfalls({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(
+ i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])
+ );
+}
+
+function addWaterfall(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, ra, getAdjective} = utils;
+
+ const descriptions = [
+ "A gorgeous waterfall flows here.",
+ "The rapids of an exceptionally beautiful waterfall.",
+ "An impressive waterfall has cut through the land.",
+ "The cascades of a stunning waterfall.",
+ "A river drops down from a great height forming a wonderous waterfall.",
+ "A breathtaking waterfall cuts through the landscape."
+ ];
+
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({id, name: getAdjective(proper) + " Waterfall", legend: `${ra(descriptions)}`});
+}
+
+function listBattlefields({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(
+ i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25
+ );
+}
+
+function addBattlefield(id, cell, pack, notes, utils) {
+ const {cells, states} = pack;
+ const {Names, BurgsAndStates, ra, generateDate, options} = utils;
+
+ const state = states[cells.state[cell]];
+ if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
+ const campaign = ra(state.campaigns);
+ const date = generateDate(campaign.start, campaign.end);
+ const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
+ const legend = `A historical battle of the ${campaign.name}. \\r\\nDate: ${date} ${options.era}.`;
+ notes.push({id, name, legend});
+}
+
+function listDungeons({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.pop[i] && cells.pop[i] < 3);
+}
+
+function addDungeon(id, cell, pack, notes, utils) {
+ const {seed} = utils;
+ const dungeonSeed = `${seed}${cell}`;
+ const name = "Dungeon";
+ const legend = ``;
+ notes.push({id, name, legend});
+}
+
+function listLakeMonsters({features}, utils) {
+ const occupied = [];
+ return features
+ .filter(feature => feature.type === "lake" && feature.group === "freshwater" && !occupied[feature.firstCell])
+ .map(feature => feature.firstCell);
+}
+
+function addLakeMonster(id, cell, pack, notes, utils) {
+ const {gauss, ra, heightUnit} = utils;
+ const lake = pack.features[pack.cells.f[cell]];
+
+ // Check that the feature is a lake in case the user clicked on a wrong square
+ if (lake.type !== "lake") return;
+
+ const name = `${lake.name} Monster`;
+ const length = gauss(10, 5, 5, 100);
+ const subjects = [
+ "Locals", "Elders", "Inscriptions", "Tipplers", "Legends", "Whispers", "Rumors", "Journeying folk", "Tales"
+ ];
+ const legend = `${ra(subjects)} say a relic monster of ${length} ${heightUnit.value} long inhabits ${
+ lake.name
+ } Lake. Truth or lie, folks are afraid to fish in the lake.`;
+ notes.push({id, name, legend});
+}
+
+function listSeaMonsters({cells, features}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(
+ i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i) && features[cells.f[i]].type === "ocean"
+ );
+}
+
+function addSeaMonster(id, cell, pack, notes, utils) {
+ const {Names, gauss, heightUnit} = utils;
+ const name = `${Names.getCultureShort(0)} Monster`;
+ const length = gauss(25, 10, 10, 100);
+ const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long.`;
+ notes.push({id, name, legend});
+}
+
+function listHillMonsters({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+}
+
+function addHillMonster(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, ra} = utils;
+
+ const adjectives = [
+ "great", "big", "huge", "prime", "golden", "proud", "lucky", "fat", "giant", "hungry",
+ "magical", "superior", "terrifying", "horrifying", "feared"
+ ];
+ const subjects = [
+ "Locals", "Elders", "Inscriptions", "Tipplers", "Legends", "Whispers", "Rumors", "Journeying folk", "Tales"
+ ];
+ const species = [
+ "Ogre", "Troll", "Cyclops", "Giant", "Monster", "Beast", "Dragon", "Undead", "Ghoul",
+ "Vampire", "Hag", "Banshee", "Bearded Devil", "Roc", "Hydra", "Warg"
+ ];
+ const modusOperandi = [
+ "steals cattle at night", "prefers eating children", "doesn't mind human flesh", "keeps the region at bay",
+ "eats kids whole", "abducts young women", "terrorizes the region", "harasses travelers in the area",
+ "snatches people from homes", "attacks anyone who dares to approach its lair", "attacks unsuspecting victims"
+ ];
+
+ const monster = ra(species);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const name = `${toponym} ${monster}`;
+ const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(
+ modusOperandi
+ )}.`;
+ notes.push({id, name, legend});
+}
+
+// Sacred mountains spawn on lonely mountains
+function listSacredMountains({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.h[i] >= 70 &&
+ cells.c[i].some(c => cells.culture[c]) &&
+ cells.c[i].every(c => cells.h[c] < 60)
+ );
+}
+
+function addSacredMountain(id, cell, pack, notes, utils) {
+ const {cells, religions} = pack;
+ const {Names, getFriendlyHeight} = utils;
+
+ const culture = cells.c[cell].map(c => cells.culture[c]).find(c => c);
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Mountain`;
+ const height = getFriendlyHeight(cells.p[cell]);
+ const legend = `A sacred mountain of ${religions[religion].name}. Height: ${height}.`;
+ notes.push({id, name, legend});
+}
+
+// Sacred forests spawn on temperate forests
+function listSacredForests({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(
+ i => !occupied[i] && cells.culture[i] && cells.religion[i] && [6, 8].includes(cells.biome[i])
+ );
+}
+
+function addSacredForest(id, cell, pack, notes, utils) {
+ const {cells, religions} = pack;
+ const {Names} = utils;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Forest`;
+ const legend = `A forest sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+}
+
+// Sacred pineries spawn on boreal forests
+function listSacredPineries({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.religion[i] && cells.biome[i] === 9);
+}
+
+function addSacredPinery(id, cell, pack, notes, utils) {
+ const {cells, religions} = pack;
+ const {Names} = utils;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Pinery`;
+ const legend = `A pinery sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+}
+
+// Sacred palm groves spawn on oasises
+function listSacredPalmGroves({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.culture[i] &&
+ cells.religion[i] &&
+ cells.biome[i] === 1 &&
+ cells.pop[i] > 1 &&
+ Routes.isConnected(i)
+ );
+}
+
+function addSacredPalmGrove(id, cell, pack, notes, utils) {
+ const {cells, religions} = pack;
+ const {Names} = utils;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Palm Grove`;
+ const legend = `A palm grove sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+}
+
+function listBrigands({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && Routes.hasRoad(i));
+}
+
+function addBrigands(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, ra, rw} = utils;
+
+ const animals = [
+ "Apes", "Badgers", "Bears", "Beavers", "Bisons", "Boars", "Cats", "Crows", "Dogs", "Foxes",
+ "Hares", "Hawks", "Hyenas", "Jackals", "Jaguars", "Leopards", "Lions", "Owls", "Panthers",
+ "Rats", "Ravens", "Rooks", "Scorpions", "Sharks", "Snakes", "Spiders", "Tigers", "Wolfs",
+ "Wolverines", "Falcons"
+ ];
+ const types = {brigands: 4, bandits: 3, robbers: 1, highwaymen: 1};
+
+ const culture = cells.culture[cell];
+ const biome = cells.biome[cell];
+ const height = cells.p[cell];
+
+ const locality = ((height, biome) => {
+ if (height >= 70) return "highlander";
+ if ([1, 2].includes(biome)) return "desert";
+ if ([3, 4].includes(biome)) return "mounted";
+ if ([5, 6, 7, 8, 9].includes(biome)) return "forest";
+ if (biome === 12) return "swamp";
+ return "angry";
+ })(height, biome);
+
+ const name = `${Names.getCulture(culture)} ${ra(animals)}`;
+ const legend = `A gang of ${locality} ${rw(types)}.`;
+ notes.push({id, name, legend});
+}
+
+// Pirates spawn on sea routes
+function listPirates({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i));
+}
+
+function addPirates(id, cell, pack, notes, utils) {
+ const name = "Pirates";
+ const legend = "Pirate ships have been spotted in these waters.";
+ notes.push({id, name, legend});
+}
+
+function listStatues({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.h[i] < 40);
+}
+
+function addStatue(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, ra, rand} = utils;
+
+ const variants = [
+ "Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone", "Sculpture", "Effigy", "Idol"
+ ];
+ const scripts = {
+ cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
+ geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
+ coptic: "ⲲⲴⲶⲸⲺⲼⲾⳀⳁⳂⳃⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳤ⳥⳧⳩⳪ⳫⳬⳭⳲ⳹⳾ ",
+ tibetan: "ༀ༁༂༃༄༅༆༇༈༉༊་༌༐༑༒༓༔༕༖༗༘༙༚༛༜༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿",
+ mongolian: "᠀᠐᠑᠒ᠠᠡᠦᠧᠨᠩᠪᠭᠮᠯᠰᠱᠲᠳᠵᠻᠼᠽᠾᠿᡀᡁᡆᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡙᡜᡝᡞᡟᡠᡡᡭᡮᡯᡰᡱᡲᡳᡴᢀᢁᢂᢋᢏᢐᢑᢒᢓᢛᢜᢞᢟᢠᢡᢢᢤᢥᢦ"
+ };
+
+ const culture = cells.culture[cell];
+
+ const variant = ra(variants);
+ const name = `${Names.getCulture(culture)} ${variant}`;
+ const script = scripts[ra(Object.keys(scripts))];
+ const inscription = Array(rand(40, 100))
+ .fill(null)
+ .map(() => ra(script))
+ .join("");
+ const legend = `An ancient ${variant.toLowerCase()}. It has an inscription, but no one can translate it:
+ ${inscription}
`;
+ notes.push({id, name, legend});
+}
+
+function listRuins({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60);
+}
+
+function addRuins(id, cell, pack, notes, utils) {
+ const {ra} = utils;
+ const types = [
+ "City", "Town", "Settlement", "Pyramid", "Fort", "Stronghold", "Temple", "Sacred site",
+ "Mausoleum", "Outpost", "Fortification", "Fortress", "Castle"
+ ];
+
+ const ruinType = ra(types);
+ const name = `Ruined ${ruinType}`;
+ const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
+ notes.push({id, name, legend});
+}
+
+function listLibraries({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.burg[i] && cells.pop[i] > 10);
+}
+
+function addLibrary(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, rw} = utils;
+
+ const type = rw({Library: 3, Archive: 1, Collection: 1});
+ const name = `${Names.getCulture(cells.culture[cell])} ${type}`;
+ const legend = "A vast collection of knowledge, including many rare and ancient tomes.";
+
+ notes.push({id, name, legend});
+}
+
+function listCircuses({cells}, utils) {
+ const {Routes} = utils;
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && Routes.isConnected(i));
+}
+
+function addCircuse(id, cell, pack, notes, utils) {
+ const {ra} = utils;
+ const adjectives = [
+ "Fantastical", "Wonderous", "Incomprehensible", "Magical", "Extraordinary", "Unmissable", "World-famous", "Breathtaking"
+ ];
+
+ const adjective = ra(adjectives);
+ const name = `Travelling ${adjective} Circus`;
+ const legend = `Roll up, roll up, this ${adjective.toLowerCase()} circus is here for a limited time only.`;
+ notes.push({id, name, legend});
+}
+
+function listJousts({cells, burgs}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 20);
+}
+
+function addJoust(id, cell, pack, notes, utils) {
+ const {cells, burgs} = pack;
+ const {ra} = utils;
+ const types = ["Joust", "Competition", "Melee", "Tournament", "Contest"];
+ const virtues = ["cunning", "might", "speed", "the greats", "acumen", "brutality"];
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+ const type = ra(types);
+ const virtue = ra(virtues);
+
+ const name = `${burgName} ${type}`;
+ const legend = `Warriors from around the land gather for a ${type.toLowerCase()} of ${virtue} in ${burgName}, with fame, fortune and favour on offer to the victor.`;
+ notes.push({id, name, legend});
+}
+
+function listFairs({cells, burgs}, utils) {
+ const occupied = [];
+ return cells.i.filter(
+ i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population < 20 && burgs[cells.burg[i]].population < 5
+ );
+}
+
+function addFair(id, cell, pack, notes, utils) {
+ const {cells, burgs} = pack;
+ if (!cells.burg[cell]) return;
+
+ const burgName = burgs[cells.burg[cell]].name;
+ const type = "Fair";
+
+ const name = `${burgName} ${type}`;
+ const legend = `A fair is being held in ${burgName}, with all manner of local and foreign goods and services on offer.`;
+ notes.push({id, name, legend});
+}
+
+function listCanoes({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.r[i]);
+}
+
+function addCanoe(id, cell, pack, notes, utils) {
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+
+ const name = `Minor Jetty`;
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const legend = `A small location along the ${riverName} to launch boats from sits here, along with a weary looking owner, willing to sell passage along the river.`;
+ notes.push({id, name, legend});
+}
+
+function listMigrations({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] <= 2);
+}
+
+function addMigration(id, cell, pack, notes, utils) {
+ const {ra} = utils;
+ const animals = [
+ "Antelopes", "Apes", "Badgers", "Bears", "Beavers", "Bisons", "Boars", "Buffalo", "Cats", "Cranes",
+ "Crocodiles", "Crows", "Deer", "Dogs", "Eagles", "Elk", "Foxes", "Goats", "Geese", "Hares",
+ "Hawks", "Herons", "Horses", "Hyenas", "Ibises", "Jackals", "Jaguars", "Larks", "Leopards", "Lions",
+ "Mantises", "Martens", "Mooses", "Mules", "Owls", "Panthers", "Rats", "Ravens", "Rooks", "Scorpions",
+ "Sharks", "Sheep", "Snakes", "Spiders", "Tigers", "Wolves", "Wolverines", "Camels", "Falcons", "Hounds", "Oxen"
+ ];
+ const animalChoice = ra(animals);
+
+ const name = `${animalChoice} migration`;
+ const legend = `A huge group of ${animalChoice.toLowerCase()} are migrating, whether part of their annual routine, or something more extraordinary.`;
+ notes.push({id, name, legend});
+}
+
+function listDances({cells, burgs}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 15);
+}
+
+function addDances(id, cell, pack, notes, utils) {
+ const {cells, burgs} = pack;
+ const {ra} = utils;
+ const burgName = burgs[cells.burg[cell]].name;
+ const socialTypes = [
+ "gala", "dance", "performance", "ball", "soiree", "jamboree", "exhibition", "carnival",
+ "festival", "jubilee", "celebration", "gathering", "fete"
+ ];
+ const people = [
+ "great and the good", "nobility", "local elders", "foreign dignitaries", "spiritual leaders", "suspected revolutionaries"
+ ];
+ const socialType = ra(socialTypes);
+
+ const name = `${burgName} ${socialType}`;
+ const legend = `A ${socialType} has been organised at ${burgName} as a chance to gather the ${ra(
+ people
+ )} of the area together to be merry, make alliances and scheme around the crisis.`;
+ notes.push({id, name, legend});
+}
+
+function listMirage({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.biome[i] === 1);
+}
+
+function addMirage(id, cell, pack, notes, utils) {
+ const {ra} = utils;
+ const adjectives = ["Entrancing", "Diaphanous", "Illusory", "Distant", "Perculiar"];
+
+ const mirageAdjective = ra(adjectives);
+ const name = `${mirageAdjective} mirage`;
+ const legend = `This ${mirageAdjective.toLowerCase()} mirage has been luring travellers out of their way for eons.`;
+ notes.push({id, name, legend});
+}
+
+function listCaves({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+}
+
+function addCave(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, rw} = utils;
+
+ const formations = {
+ Cave: 10, Cavern: 8, Chasm: 6, Ravine: 6, Fracture: 5, Grotto: 4, Pit: 4, Sinkhole: 2, Hole: 2
+ };
+ const status = {
+ "a good spot to hid treasure": 5,
+ "the home of strange monsters": 5,
+ "totally empty": 4,
+ "endlessly deep and unexplored": 4,
+ "completely flooded": 2,
+ "slowly filling with lava": 1
+ };
+
+ let formation = rw(formations);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ if (cells.biome[cell] === 11) {
+ formation = "Glacial " + formation;
+ }
+ const name = `${toponym} ${formation}`;
+ const legend = `The ${name}. Locals claim that it is ${rw(status)}.`;
+ notes.push({id, name, legend});
+}
+
+function listPortals({burgs}, utils) {
+ const occupied = [];
+ return burgs
+ .slice(1, Math.ceil(burgs.length / 10) + 1)
+ .filter(({cell}) => !occupied[cell])
+ .map(burg => burg.cell);
+}
+
+function addPortal(id, cell, pack, notes, utils) {
+ const {cells, burgs} = pack;
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+
+ const name = `${burgName} Portal`;
+ const legend = `An element of the magic portal system connecting major cities. The portals were installed centuries ago, but still work fine.`;
+ notes.push({id, name, legend});
+}
+
+function listRifts({cells}, utils) {
+ const {biomesData} = utils;
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && pack.cells.pop[i] <= 3 && biomesData.habitability[pack.cells.biome[i]]);
+}
+
+function addRift(id, cell, pack, notes, utils) {
+ const {ra} = utils;
+ const types = ["Demonic", "Interdimensional", "Abyssal", "Cosmic", "Cataclysmic", "Subterranean", "Ancient"];
+
+ const descriptions = [
+ "all known nearby beings to flee in terror",
+ "cracks in reality itself to form",
+ "swarms of foes to spill forth",
+ "nearby plants to wither and decay",
+ "an emmissary to step through with an all-powerful relic"
+ ];
+
+ const riftType = ra(types);
+ const name = `${riftType} Rift`;
+ const legend = `A rumoured ${riftType.toLowerCase()} rift in this area is causing ${ra(descriptions)}.`;
+ notes.push({id, name, legend});
+}
+
+function listDisturbedBurial({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 2);
+}
+function addDisturbedBurial(id, cell, pack, notes, utils) {
+ const name = "Disturbed Burial";
+ const legend = "A burial site has been disturbed in this area, causing the dead to rise and attack the living.";
+ notes.push({id, name, legend});
+}
+
+function listNecropolis({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] < 2);
+}
+
+function addNecropolis(id, cell, pack, notes, utils) {
+ const {cells} = pack;
+ const {Names, rw, ra} = utils;
+
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const type = rw({
+ Necropolis: 5, Crypt: 2, Tomb: 2, Graveyard: 1, Cemetery: 2, Mausoleum: 1, Sepulchre: 1
+ });
+
+ const name = `${toponym} ${type}`;
+ const legend = ra([
+ "A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls.",
+ "A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics.",
+ "This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed.",
+ "Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets.",
+ "An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers.",
+ "A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths.",
+ "This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls.",
+ "A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air.",
+ "A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners.",
+ "A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses."
+ ]);
+
+ notes.push({id, name, legend});
+}
+
+function listEncounters({cells}, utils) {
+ const occupied = [];
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 1);
+}
+
+function addEncounter(id, cell, pack, notes, utils) {
+ const name = "Random encounter";
+ const encounterSeed = cell; // use just cell Id to not overwhelm the Vercel KV database
+ const legend = `You have encountered a character.
`;
+ notes.push({id, name, legend});
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/military-generator.js b/procedural/src/engine/modules/military-generator.js
new file mode 100644
index 00000000..ea72ff6c
--- /dev/null
+++ b/procedural/src/engine/modules/military-generator.js
@@ -0,0 +1,392 @@
+"use strict";
+
+export function generate(pack, config, utils, notes) {
+ const {TIME, minmax, rn, ra, rand, gauss, si, nth, d3, populationRate, urbanization} = utils;
+
+ TIME && console.time("generateMilitary");
+ const {cells, states, burgs, provinces} = pack;
+ const {p} = cells;
+ const valid = states.filter(s => s.i && !s.removed); // valid states
+ const military = config.military || getDefaultOptions();
+
+ const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
+ const area = d3.sum(valid.map(s => s.area)); // total area
+ const rate = {
+ x: 0,
+ Ally: -0.2,
+ Friendly: -0.1,
+ Neutral: 0,
+ Suspicion: 0.1,
+ Enemy: 1,
+ Unknown: 0,
+ Rival: 0.5,
+ Vassal: 0.5,
+ Suzerain: -0.5
+ };
+
+ const stateModifier = {
+ melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1},
+ ranged: {Nomadic: 0.9, Highland: 1.3, Lake: 1, Naval: 0.8, Hunting: 2, River: 0.8},
+ mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
+ machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
+ naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
+ armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
+ aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
+ magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
+ };
+
+ const cellTypeModifier = {
+ nomadic: {
+ melee: 0.2,
+ ranged: 0.5,
+ mounted: 3,
+ machinery: 0.4,
+ naval: 0.3,
+ armored: 1.6,
+ aviation: 1,
+ magical: 0.5
+ },
+ wetland: {
+ melee: 0.8,
+ ranged: 2,
+ mounted: 0.3,
+ machinery: 1.2,
+ naval: 1.0,
+ armored: 0.2,
+ aviation: 0.5,
+ magical: 0.5
+ },
+ highland: {
+ melee: 1.2,
+ ranged: 1.6,
+ mounted: 0.3,
+ machinery: 3,
+ naval: 1.0,
+ armored: 0.8,
+ aviation: 0.3,
+ magical: 2
+ }
+ };
+
+ const burgTypeModifier = {
+ nomadic: {
+ melee: 0.3,
+ ranged: 0.8,
+ mounted: 3,
+ machinery: 0.4,
+ naval: 1.0,
+ armored: 1.6,
+ aviation: 1,
+ magical: 0.5
+ },
+ wetland: {
+ melee: 1,
+ ranged: 1.6,
+ mounted: 0.2,
+ machinery: 1.2,
+ naval: 1.0,
+ armored: 0.2,
+ aviation: 0.5,
+ magical: 0.5
+ },
+ highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
+ };
+
+ valid.forEach(s => {
+ s.temp = {};
+ const d = s.diplomacy;
+
+ const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
+ const diplomacyRate = d.some(d => d === "Enemy")
+ ? 1
+ : d.some(d => d === "Rival")
+ ? 0.8
+ : d.some(d => d === "Suspicion")
+ ? 0.5
+ : 0.1; // peacefulness
+ const neighborsRateRaw = s.neighbors
+ .map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion"))
+ .reduce((s, r) => (s += rate[r]), 0.5);
+ const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
+ s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
+ s.temp.platoons = [];
+
+ // apply overall state modifiers for unit types based on state features
+ for (const unit of military) {
+ if (!stateModifier[unit.type]) continue;
+
+ let modifier = stateModifier[unit.type][s.type] || 1;
+ if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
+ else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
+ s.temp[unit.name] = modifier * s.alert;
+ }
+ });
+
+ const getType = cell => {
+ if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
+ if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
+ if (cells.h[cell] >= 70) return "highland";
+ return "generic";
+ };
+
+ function passUnitLimits(unit, biome, state, culture, religion) {
+ if (unit.biomes && !unit.biomes.includes(biome)) return false;
+ if (unit.states && !unit.states.includes(state)) return false;
+ if (unit.cultures && !unit.cultures.includes(culture)) return false;
+ if (unit.religions && !unit.religions.includes(religion)) return false;
+ return true;
+ }
+
+ // rural cells
+ for (const i of cells.i) {
+ if (!cells.pop[i]) continue;
+
+ const biome = cells.biome[i];
+ const state = cells.state[i];
+ const culture = cells.culture[i];
+ const religion = cells.religion[i];
+
+ const stateObj = states[state];
+ if (!state || stateObj.removed) continue;
+
+ let modifier = cells.pop[i] / 100; // basic rural army in percentages
+ if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center])
+ modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
+ if (cells.f[i] !== cells.f[stateObj.center])
+ modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
+ const type = getType(i);
+
+ for (const unit of military) {
+ const perc = +unit.rural;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && !cells.haven[i]) continue; // only near-ocean cells create naval units
+
+ const cellTypeMod = type === "generic" ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
+ const army = modifier * perc * cellTypeMod; // rural cell army
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[i];
+ let n = 0;
+
+ // place naval units to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[i];
+ [x, y] = p[haven];
+ n = 1;
+ }
+
+ stateObj.temp.platoons.push({
+ cell: i,
+ a: total,
+ t: total,
+ x,
+ y,
+ u: unit.name,
+ n,
+ s: unit.separate,
+ type: unit.type
+ });
+ }
+ }
+
+ // burgs
+ for (const b of burgs) {
+ if (!b.i || b.removed || !b.state || !b.population) continue;
+
+ const biome = cells.biome[b.cell];
+ const state = b.state;
+ const culture = b.culture;
+ const religion = cells.religion[b.cell];
+
+ const stateObj = states[state];
+ let m = (b.population * urbanization) / 100; // basic urban army in percentages
+ if (b.capital) m *= 1.2; // capital has household troops
+ if (culture !== stateObj.culture) m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center]) m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
+ if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
+ const type = getType(b.cell);
+
+ for (const unit of military) {
+ const perc = +unit.urban;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && (!b.port || !cells.haven[b.cell])) continue; // only ports create naval units
+
+ const mod = type === "generic" ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
+ const army = m * perc * mod; // urban cell army
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[b.cell];
+ let n = 0;
+
+ // place naval to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[b.cell];
+ [x, y] = p[haven];
+ n = 1;
+ }
+
+ stateObj.temp.platoons.push({
+ cell: b.cell,
+ a: total,
+ t: total,
+ x,
+ y,
+ u: unit.name,
+ n,
+ s: unit.separate,
+ type: unit.type
+ });
+ }
+ }
+
+ const expected = 3 * populationRate; // expected regiment size
+ const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
+
+ // get regiments for each state
+ valid.forEach(s => {
+ s.military = createRegiments(s.temp.platoons, s, pack, config, utils, notes);
+ delete s.temp; // do not store temp data
+ });
+
+ function createRegiments(nodes, s, pack, config, utils, notes) {
+ if (!nodes.length) return [];
+
+ nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
+ const tree = d3.quadtree(
+ nodes,
+ d => d.x,
+ d => d.y
+ );
+
+ nodes.forEach(node => {
+ tree.remove(node);
+ const overlap = tree.find(node.x, node.y, 20);
+ if (overlap && overlap.t && mergeable(node, overlap)) {
+ merge(node, overlap);
+ return;
+ }
+ if (node.t > expected) return;
+ const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
+ const candidates = tree.findAll(node.x, node.y, r);
+ for (const c of candidates) {
+ if (c.t < expected && mergeable(node, c)) {
+ merge(node, c);
+ break;
+ }
+ }
+ });
+
+ // add n0 to n1's ultimate parent
+ function merge(n0, n1) {
+ if (!n1.childen) n1.childen = [n0];
+ else n1.childen.push(n0);
+ if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
+ n1.t += n0.t;
+ n0.t = 0;
+ }
+
+ // parse regiments data
+ const regiments = nodes
+ .filter(n => n.t)
+ .sort((a, b) => b.t - a.t)
+ .map((r, i) => {
+ const u = {};
+ u[r.u] = r.a;
+ (r.childen || []).forEach(n => (u[n.u] = u[n.u] ? (u[n.u] += n.a) : n.a));
+ return {i, a: r.t, cell: r.cell, x: r.x, y: r.y, bx: r.x, by: r.y, u, n: r.n, name: null, state: s.i};
+ });
+
+ // generate name for regiments
+ regiments.forEach(r => {
+ r.name = getName(r, regiments, pack, utils);
+ r.icon = getEmblem(r, pack, config);
+ generateNote(r, s, pack, config, utils, notes);
+ });
+
+ return regiments;
+ }
+
+ TIME && console.timeEnd("generateMilitary");
+
+ return pack;
+}
+
+export function getDefaultOptions() {
+ return [
+ {icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
+ {icon: "🏹", name: "archers", rural: 0.12, urban: 0.2, crew: 1, power: 1, type: "ranged", separate: 0},
+ {icon: "🐴", name: "cavalry", rural: 0.12, urban: 0.03, crew: 2, power: 2, type: "mounted", separate: 0},
+ {icon: "💣", name: "artillery", rural: 0, urban: 0.03, crew: 8, power: 12, type: "machinery", separate: 0},
+ {icon: "🌊", name: "fleet", rural: 0, urban: 0.015, crew: 100, power: 50, type: "naval", separate: 1}
+ ];
+}
+
+// utilize si function to make regiment total text fit regiment box
+export function getTotal(reg, utils) {
+ const {si} = utils;
+ return reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a;
+}
+
+export function getName(r, regiments, pack, utils) {
+ const {nth} = utils;
+ const {cells, provinces, burgs} = pack;
+ const proper = r.n
+ ? null
+ : cells.province[r.cell] && provinces[cells.province[r.cell]]
+ ? provinces[cells.province[r.cell]].name
+ : cells.burg[r.cell] && burgs[cells.burg[r.cell]]
+ ? burgs[cells.burg[r.cell]].name
+ : null;
+ const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
+ const form = r.n ? "Fleet" : "Regiment";
+ return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
+}
+
+// get default regiment emblem
+export function getEmblem(r, pack, config) {
+ if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
+ if (
+ !r.n &&
+ pack.states[r.state].form === "Monarchy" &&
+ pack.cells.burg[r.cell] &&
+ pack.burgs[pack.cells.burg[r.cell]].capital
+ )
+ return "👑"; // "Royal" regiment based in capital
+ const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
+ const unit = config.military.find(u => u.name === mainUnit);
+ return unit.icon;
+}
+
+export function generateNote(r, s, pack, config, utils, notes) {
+ const {ra, rand, gauss} = utils;
+ const {cells, burgs, provinces} = pack;
+ const base =
+ cells.burg[r.cell] && burgs[cells.burg[r.cell]]
+ ? burgs[cells.burg[r.cell]].name
+ : cells.province[r.cell] && provinces[cells.province[r.cell]]
+ ? provinces[cells.province[r.cell]].fullName
+ : null;
+ const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
+
+ const composition = r.a
+ ? Object.keys(r.u)
+ .map(t => `— ${t}: ${r.u[t]}`)
+ .join("\r\n")
+ : null;
+ const troops = composition
+ ? `\r\n\r\nRegiment composition in ${config.year} ${config.eraShort}:\r\n${composition}.`
+ : "";
+
+ const campaign = s.campaigns ? ra(s.campaigns) : null;
+ const year = campaign
+ ? rand(campaign.start, campaign.end || config.year)
+ : gauss(config.year - 100, 150, 1, config.year - 6);
+ const conflict = campaign ? ` during the ${campaign.name}` : "";
+ const legend = `Regiment was formed in ${year} ${config.era}${conflict}. ${station}${troops}`;
+ notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/names-generator.js b/procedural/src/engine/modules/names-generator.js
new file mode 100644
index 00000000..cd04a95a
--- /dev/null
+++ b/procedural/src/engine/modules/names-generator.js
@@ -0,0 +1,335 @@
+"use strict";
+
+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, nameBases) => {
+ 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, nameBases, utils) {
+ const { ERROR, ra, last, P } = utils;
+
+ if (base === undefined) return ERROR && console.error("Please define a base");
+
+ if (nameBases[base] === undefined) {
+ if (nameBases[0]) {
+ ERROR && 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, nameBases);
+
+ const data = chains[base];
+ if (!data || data[""] === undefined) {
+ 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, cultures, nameBases, utils) {
+ const { ERROR } = utils;
+
+ if (culture === undefined) return ERROR && console.error("Please define a culture");
+ const base = cultures[culture].base;
+ return getBase(base, min, max, dupl, nameBases, utils);
+};
+
+// generate short name for culture
+const getCultureShort = function (culture, cultures, nameBases, utils) {
+ const { ERROR } = utils;
+
+ if (culture === undefined) return ERROR && console.error("Please define a culture");
+ return getBaseShort(cultures[culture].base, nameBases, utils);
+};
+
+// generate short name for base
+const getBaseShort = function (base, nameBases, utils) {
+ 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, "", nameBases, utils);
+};
+
+// generate state name based on capital or random name and culture-specific suffix
+const getState = function (name, culture, base, cultures, nameBases, utils) {
+ const { ERROR, P, capitalize, vowel } = utils;
+
+ 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 = 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, utils);
+};
+
+function validateSuffix(name, suffix, utils) {
+ const { vowel } = utils;
+
+ 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;
+}
+
+// generate name for the map
+const getMapName = function (nameBases, config, utils) {
+ const { P, rand } = utils;
+
+ const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31);
+ if (!nameBases[base]) {
+ return "";
+ }
+ const min = nameBases[base].min - 1;
+ const max = Math.max(nameBases[base].max - 3, min);
+ const baseName = getBase(base, min, max, "", nameBases, utils);
+ const name = P(0.7) ? addSuffix(baseName, utils) : baseName;
+ return name;
+};
+
+function addSuffix(name, utils) {
+ const { P } = utils;
+
+ 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, utils);
+}
+
+const getNameBases = function () {
+ // name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
+ // prettier-ignore
+ return [
+ {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"}
+ ];
+};
+
+export {
+ getBase,
+ getCulture,
+ getCultureShort,
+ getBaseShort,
+ getState,
+ updateChain,
+ clearChains,
+ getNameBases,
+ getMapName,
+ calculateChain
+};
\ No newline at end of file
diff --git a/procedural/src/engine/modules/ocean-layers.js b/procedural/src/engine/modules/ocean-layers.js
new file mode 100644
index 00000000..cc9a8fb5
--- /dev/null
+++ b/procedural/src/engine/modules/ocean-layers.js
@@ -0,0 +1,94 @@
+"use strict";
+
+export function generateOceanLayers(grid, config, utils) {
+ const { lineGen, clipPoly, round, rn, P } = utils;
+
+ if (config.outline === "none") return { layers: [] };
+
+ const cells = grid.cells;
+ const pointsN = grid.cells.i.length;
+ const vertices = grid.vertices;
+ const limits = config.outline === "random" ? randomizeOutline(P) : config.outline.split(",").map(s => +s);
+
+ const chains = [];
+ const opacity = rn(0.4 / limits.length, 2);
+ const 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, cells, vertices, pointsN);
+ if (!start) continue;
+ used[i] = 1;
+ const chain = connectVertices(start, t, cells, vertices, pointsN, used); // 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]);
+ }
+
+ const layers = [];
+ for (const t of limits) {
+ const layer = chains.filter(c => c[0] === t);
+ const paths = layer.map(c => round(lineGen(c[1]))).filter(path => path);
+ if (paths.length > 0) {
+ layers.push({
+ type: t,
+ paths: paths,
+ opacity: opacity
+ });
+ }
+ }
+
+ return { layers };
+}
+
+function randomizeOutline(P) {
+ 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;
+}
+
+// find eligible cell vertex to start path detection
+function findStart(i, t, cells, vertices, pointsN) {
+ 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])];
+}
+
+// connect vertices to chain
+function connectVertices(start, t, cells, vertices, pointsN, used) {
+ 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]) {
+ console.error("Next vertex is not found");
+ break;
+ }
+ }
+ chain.push(chain[0]); // push first vertex as the last one
+ return chain;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/provinces-generator.js b/procedural/src/engine/modules/provinces-generator.js
new file mode 100644
index 00000000..2202f489
--- /dev/null
+++ b/procedural/src/engine/modules/provinces-generator.js
@@ -0,0 +1,278 @@
+"use strict";
+
+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}
+};
+
+export const generate = (pack, config, utils, regenerate = false, regenerateLockedStates = false) => {
+ const {
+ TIME,
+ generateSeed,
+ aleaPRNG,
+ gauss,
+ P,
+ Names,
+ rw,
+ getMixedColor,
+ BurgsAndStates,
+ COA,
+ FlatQueue,
+ d3,
+ rand
+ } = utils;
+
+ TIME && console.time("generateProvinces");
+ const localSeed = regenerate ? generateSeed() : config.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 = config.provincesRatio;
+ const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
+
+ // generate provinces for selected burgs
+ states.forEach(s => {
+ s.provinces = [];
+ if (!s.i || s.removed) return;
+ if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
+ if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
+
+ const stateBurgs = burgs
+ .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
+ .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
+ .sort((a, b) => b.capital - a.capital);
+ if (stateBurgs.length < 2) return; // at least 2 provinces are required
+ const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
+
+ const form = Object.assign({}, forms[s.form]);
+
+ for (let i = 0; i < provincesNumber; i++) {
+ const provinceId = provinces.length;
+ const center = stateBurgs[i].cell;
+ const burg = stateBurgs[i].i;
+ const c = stateBurgs[i].culture;
+ const nameByBurg = P(0.5);
+ const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
+ const formName = rw(form);
+ form[formName] += 10;
+ const fullName = name + " " + formName;
+ const color = getMixedColor(s.color);
+ const kinship = nameByBurg ? 0.8 : 0.4;
+ const type = BurgsAndStates.getType(center, burg.port);
+ const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ s.provinces.push(provinceId);
+ provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
+ }
+ });
+
+ // expand generated provinces
+ const queue = new FlatQueue();
+ const cost = [];
+
+ provinces.forEach(p => {
+ if (!p.i || p.removed || isProvinceLocked(p)) return;
+ provinceIds[p.center] = p.i;
+ queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
+ cost[p.center] = 1;
+ });
+
+ while (queue.length) {
+ const {e, p, province, state} = queue.pop();
+
+ cells.c[e].forEach(e => {
+ if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
+
+ const land = cells.h[e] >= 20;
+ if (!land && !cells.t[e]) return; // cannot pass deep ocean
+ if (land && cells.state[e] !== state) return;
+ const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
+ const totalCost = p + evevation;
+
+ if (totalCost > max) return;
+ if (!cost[e] || totalCost < cost[e]) {
+ if (land) provinceIds[e] = province; // assign province to a cell
+ cost[e] = totalCost;
+ queue.push({e, province, state, p: totalCost}, totalCost);
+ }
+ });
+ }
+
+ // justify provinces shapes a bit
+ for (const i of cells.i) {
+ if (cells.burg[i]) continue; // do not overwrite burgs
+ if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
+
+ const neibs = cells.c[i]
+ .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
+ .map(c => provinceIds[c]);
+ const adversaries = neibs.filter(c => c !== provinceIds[i]);
+ if (adversaries.length < 2) continue;
+
+ const buddies = neibs.filter(c => c === provinceIds[i]).length;
+ if (buddies.length > 2) continue;
+
+ const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
+ const max = d3.max(competitors);
+ if (buddies >= max) continue;
+
+ provinceIds[i] = adversaries[competitors.indexOf(max)];
+ }
+
+ // add "wild" provinces if some cells don't have a province assigned
+ const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
+ states.forEach(s => {
+ if (!s.i || s.removed) return;
+ if (s.lock && !regenerateLockedStates) return;
+ if (!s.provinces.length) return;
+
+ const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
+ const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
+ const getColonyName = () => {
+ if (colonyNamePool.length < 1) return null;
+
+ const index = rand(colonyNamePool.length - 1);
+ const spliced = colonyNamePool.splice(index, 1);
+ return spliced[0] ? `New ${spliced[0]}` : null;
+ };
+
+ let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ while (stateNoProvince.length) {
+ // add new province
+ const provinceId = provinces.length;
+ const burgCell = stateNoProvince.find(i => cells.burg[i]);
+ const center = burgCell ? burgCell : stateNoProvince[0];
+ const burg = burgCell ? cells.burg[burgCell] : 0;
+ provinceIds[center] = provinceId;
+
+ // expand province
+ const cost = [];
+ cost[center] = 1;
+ queue.push({e: center, p: 0}, 0);
+ while (queue.length) {
+ const {e, p} = queue.pop();
+
+ cells.c[e].forEach(nextCellId => {
+ if (provinceIds[nextCellId]) return;
+ const land = cells.h[nextCellId] >= 20;
+ if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
+ const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
+ const totalCost = p + ter;
+
+ if (totalCost > max) return;
+ if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
+ if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
+ cost[nextCellId] = totalCost;
+ queue.push({e: nextCellId, p: totalCost}, totalCost);
+ }
+ });
+ }
+
+ // generate "wild" province name
+ const c = cells.culture[center];
+ const f = pack.features[cells.f[center]];
+ const color = getMixedColor(s.color);
+
+ const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
+ const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
+ const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
+ const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
+
+ const name = (() => {
+ const colonyName = colony && P(0.8) && getColonyName();
+ if (colonyName) return colonyName;
+ if (burgCell && P(0.5)) return burgs[burg].name;
+ return Names.getState(Names.getCultureShort(c), c);
+ })();
+
+ const formName = (() => {
+ if (singleIsle) return "Island";
+ if (isleGroup) return "Islands";
+ if (colony) return "Colony";
+ return rw(forms["Wild"]);
+ })();
+
+ const fullName = name + " " + formName;
+
+ const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
+ const kinship = dominion ? 0 : 0.4;
+ const type = BurgsAndStates.getType(center, burgs[burg]?.port);
+ const coa = COA.generate(s.coa, kinship, dominion, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
+ s.provinces.push(provinceId);
+
+ // check if there is a land way within the same state between two cells
+ function isPassable(from, to) {
+ if (cells.f[from] !== cells.f[to]) return false; // on different islands
+ const passableQueue = [from],
+ used = new Uint8Array(cells.i.length),
+ state = cells.state[from];
+ while (passableQueue.length) {
+ const current = passableQueue.pop();
+ if (current === to) return true; // way is found
+ cells.c[current].forEach(c => {
+ if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
+ passableQueue.push(c);
+ used[c] = 1;
+ });
+ }
+ return false; // way is not found
+ }
+
+ // re-check
+ stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ }
+ });
+
+ TIME && console.timeEnd("generateProvinces");
+
+ return {
+ provinces,
+ provinceIds
+ };
+};
+
+// calculate pole of inaccessibility for each province
+export const getPoles = (pack, utils) => {
+ const { getPolesOfInaccessibility } = utils;
+
+ const getType = cellId => pack.cells.province[cellId];
+ const poles = getPolesOfInaccessibility(pack, getType);
+
+ const updatedProvinces = pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ return {
+ ...province,
+ pole: poles[province.i] || [0, 0]
+ };
+ });
+
+ return updatedProvinces;
+};
\ No newline at end of file
diff --git a/procedural/src/engine/modules/religions-generator.js b/procedural/src/engine/modules/religions-generator.js
new file mode 100644
index 00000000..1f97cb33
--- /dev/null
+++ b/procedural/src/engine/modules/religions-generator.js
@@ -0,0 +1,973 @@
+"use strict";
+
+// 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: (utils) => utils.gauss(5, 3, 0, 10, 1),
+ Cult: (utils) => utils.gauss(0.5, 0.5, 0, 5, 1),
+ Heresy: (utils) => utils.gauss(1, 0.5, 0, 5, 1)
+};
+
+export function generate(pack, grid, config, utils) {
+ const {TIME} = utils;
+ TIME && console.time("generateReligions");
+ const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
+
+ const folkReligions = generateFolkReligions(pack, utils);
+ const organizedReligions = generateOrganizedReligions(pack, grid, config.religionsNumber, lockedReligions, utils);
+
+ const namedReligions = specifyReligions(pack, [...folkReligions, ...organizedReligions], utils);
+ const indexedReligions = combineReligions(namedReligions, lockedReligions, utils);
+ const religionIds = expandReligions(pack, grid, indexedReligions, config, utils);
+ const religions = defineOrigins(pack, religionIds, indexedReligions, utils);
+
+ const newPack = {
+ ...pack,
+ religions: religions,
+ cells: {
+ ...pack.cells,
+ religion: religionIds
+ }
+ };
+
+ const finalPack = checkCenters(newPack);
+
+ TIME && console.timeEnd("generateReligions");
+ return finalPack;
+}
+
+function generateFolkReligions(pack, utils) {
+ const {rw} = utils;
+ 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(pack, grid, desiredReligionNumber, lockedReligions, utils) {
+ const {rand, rw, d3, WARN} = utils;
+ const {graphWidth, graphHeight} = grid;
+ 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(pack, newReligions, utils) {
+ const {getRandomColor, getMixedColor} = utils;
+ const {cells, cultures} = pack;
+
+ const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => {
+ const supreme = getDeityName(cultureId, utils);
+ const deity = form === "Non-theism" || form === "Animism" ? null : supreme;
+
+ const stateId = cells.state[center];
+
+ let [name, expansion] = generateReligionName(pack, type, form, supreme, center, utils);
+ if (expansion === "state" && !stateId) expansion = "global";
+
+ const expansionism = expansionismMap[type](utils);
+ 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, utils) {
+ const {abbreviate} = utils;
+ 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(pack, religionIds, indexedReligions, utils) {
+ const {each} = utils;
+ 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(pack, grid, religions, config, utils) {
+ const {FlatQueue, isWater} = utils;
+ 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) * config.growthRate;
+
+ 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, utils) {
+ const {Routes, biomesData} = utils;
+ 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(pack) {
+ const cells = pack.cells;
+ const updatedReligions = pack.religions.map(r => {
+ if (!r.i) return r;
+ // move religion center if it's not within religion area after expansion
+ if (cells.religion[r.center] === r.i) return r; // in area
+ const firstCell = cells.i.find(i => cells.religion[i] === r.i);
+ const cultureHome = pack.cultures[r.culture]?.center;
+ if (firstCell) return {...r, center: firstCell}; // move center, othervise it's an extinct religion
+ else if (r.type === "Folk" && cultureHome) return {...r, center: cultureHome}; // reset extinct culture centers
+ return r;
+ });
+
+ return {
+ ...pack,
+ religions: updatedReligions
+ };
+}
+
+export function recalculate(pack, grid, config, utils) {
+ const newReligionIds = expandReligions(pack, grid, pack.religions, config, utils);
+ const newPack = {
+ ...pack,
+ cells: {
+ ...pack.cells,
+ religion: newReligionIds
+ }
+ };
+
+ return checkCenters(newPack);
+}
+
+export function add(pack, center, utils) {
+ const {rw, getMixedColor, abbreviate} = utils;
+ 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, utils);
+
+ const [name, expansion] = generateReligionName(pack, type, form, deity, center, utils);
+
+ 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;
+
+ const newReligion = {
+ i,
+ name,
+ color,
+ culture: cultureId,
+ type,
+ form: formName,
+ deity,
+ expansion,
+ expansionism: expansionismMap[type](utils),
+ center,
+ cells: 0,
+ area: 0,
+ rural: 0,
+ urban: 0,
+ origins,
+ code
+ };
+
+ const newReligions = [...religions, newReligion];
+ const newReligionIds = [...cells.religion];
+ newReligionIds[center] = i;
+
+ return {
+ ...pack,
+ religions: newReligions,
+ cells: {
+ ...pack.cells,
+ religion: newReligionIds
+ }
+ };
+}
+
+export function updateCultures(pack) {
+ const updatedReligions = pack.religions.map((religion, index) => {
+ if (index === 0) return religion;
+ return {...religion, culture: pack.cells.culture[religion.center]};
+ });
+
+ return {
+ ...pack,
+ religions: updatedReligions
+ };
+}
+
+// get supreme deity name
+export function getDeityName(culture, utils) {
+ const {ERROR, Names} = utils;
+ if (culture === undefined) {
+ ERROR && console.error("Please define a culture");
+ return;
+ }
+ const meaning = generateMeaning(utils);
+ const cultureName = Names.getCulture(culture, null, null, "", 0.8);
+ return cultureName + ", The " + meaning;
+}
+
+function generateMeaning(utils) {
+ const {ra, ERROR} = utils;
+ 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(pack, variety, form, deity, center, utils) {
+ const {rw, Names, ra, trimVowels, getAdjective} = utils;
+ 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(utils)}`, "global"];
+ return [trimVowels(random()) + "ism", "global"]; // else
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/resample.js b/procedural/src/engine/modules/resample.js
new file mode 100644
index 00000000..e0db3189
--- /dev/null
+++ b/procedural/src/engine/modules/resample.js
@@ -0,0 +1,410 @@
+"use strict";
+
+/*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+*/
+export function process({projection, inverse, scale}, grid, pack, notes, config, utils) {
+ const {deepCopy, generateGrid, rn, findCell, findAll, isInMap, unique, lineclip, WARN} = utils;
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(parentMap.pack.rivers, utils);
+
+ const newGrid = generateGrid();
+ const newPack = {};
+ const newNotes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale, newGrid, utils);
+
+ // External module calls that modify newGrid and newPack would need to be handled by caller
+ // Features.markupGrid(), addLakesInDeepDepressions(), openNearSeaLakes(),
+ // OceanLayers(), calculateMapCoordinates(), calculateTemperatures(),
+ // reGraph(), Features.markupPack(), createDefaultRuler()
+
+ const cellData = restoreCellData(parentMap, inverse, scale, newPack, config, utils);
+ const rivers = restoreRivers(riversData, projection, scale, newPack, config, utils);
+ const cultures = restoreCultures(parentMap, projection, newPack, utils);
+ const burgs = restoreBurgs(parentMap, projection, scale, newPack, utils);
+ const states = restoreStates(parentMap, projection, newPack, config, utils);
+ const routes = restoreRoutes(parentMap, projection, newPack, config, utils);
+ const religions = restoreReligions(parentMap, projection, newPack, utils);
+ const provinces = restoreProvinces(parentMap, newPack, utils);
+ const featureDetails = restoreFeatureDetails(parentMap, inverse, newPack, utils);
+ const markers = restoreMarkers(parentMap, projection, newPack, utils);
+ const zones = restoreZones(parentMap, projection, scale, newPack, utils);
+
+ return {
+ grid: newGrid,
+ pack: {
+ ...newPack,
+ cells: cellData.cells,
+ rivers: rivers,
+ cultures: cultures,
+ burgs: burgs,
+ states: states,
+ routes: routes,
+ religions: religions,
+ provinces: provinces,
+ markers: markers,
+ zones: zones,
+ features: newPack.features || []
+ },
+ notes: newNotes
+ };
+}
+
+function resamplePrimaryGridData(parentMap, inverse, scale, grid, utils) {
+ const {smoothHeightmap} = utils;
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap(grid);
+}
+
+function smoothHeightmap(grid) {
+ const {d3, isWater} = grid.utils || {};
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+}
+
+function restoreCellData(parentMap, inverse, scale, pack, config, utils) {
+ const {d3, isWater} = utils;
+
+ const cells = {
+ biome: new Uint8Array(pack.cells.i.length),
+ fl: new Uint16Array(pack.cells.i.length),
+ s: new Int16Array(pack.cells.i.length),
+ pop: new Float32Array(pack.cells.i.length),
+ culture: new Uint16Array(pack.cells.i.length),
+ state: new Uint16Array(pack.cells.i.length),
+ burg: new Uint16Array(pack.cells.i.length),
+ religion: new Uint16Array(pack.cells.i.length),
+ province: new Uint16Array(pack.cells.i.length)
+ };
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+
+ return {cells};
+}
+
+function saveRiversData(parentRivers, utils) {
+ const {Rivers} = utils;
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+}
+
+function restoreRivers(riversData, projection, scale, pack, config, utils) {
+ const {rn, isInMap, findCell, Rivers} = utils;
+
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ const rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config.graphWidth, config.graphHeight);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+
+ return rivers;
+}
+
+function restoreCultures(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, getPolesOfInaccessibility} = utils;
+
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+
+ return parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+}
+
+function restoreBurgs(parentMap, projection, scale, pack, utils) {
+ const {d3, rn, isInMap, findCell, isWater, WARN, BurgsAndStates} = utils;
+
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ return parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, utils);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, utils) {
+ const {rn, BurgsAndStates} = utils;
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+}
+
+function restoreStates(parentMap, projection, pack, config, utils) {
+ const {rn, isInMap, findCell, BurgsAndStates} = utils;
+
+ const validStates = new Set(pack.cells.state);
+ let states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ states = states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords, config.graphWidth, config.graphHeight) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos, config.graphWidth, config.graphHeight) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos, config.graphWidth, config.graphHeight)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase, config.graphWidth, config.graphHeight) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+
+ return states;
+}
+
+function restoreRoutes(parentMap, projection, pack, config, utils) {
+ const {rn, isInMap, findCell, lineclip, Routes} = utils;
+
+ const routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config.graphWidth, config.graphHeight);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, config.graphWidth, config.graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(routes);
+ return routes;
+}
+
+function restoreReligions(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, getPolesOfInaccessibility} = utils;
+
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ return parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+}
+
+function restoreProvinces(parentMap, pack, utils) {
+ const {findCell, Provinces} = utils;
+
+ const validProvinces = new Set(pack.cells.province);
+ const provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+
+ return provinces;
+}
+
+function restoreMarkers(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, Markers} = utils;
+
+ const markers = parentMap.pack.markers;
+ markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+
+ return markers;
+}
+
+function restoreZones(parentMap, projection, scale, pack, utils) {
+ const {isInMap, findAll, unique} = utils;
+
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ return parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+}
+
+function restoreFeatureDetails(parentMap, inverse, pack, utils) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+
+ return pack.features;
+}
+
+function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+}
+
+function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/river-generator.js b/procedural/src/engine/modules/river-generator.js
new file mode 100644
index 00000000..7c173884
--- /dev/null
+++ b/procedural/src/engine/modules/river-generator.js
@@ -0,0 +1,559 @@
+"use strict";
+
+export const generate = function (pack, grid, config, utils, modules, allowErosion = true) {
+ const {TIME, seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config;
+ const {rn, rw, each, round, d3, lineGen} = utils;
+ const {Lakes, Names} = modules;
+
+ TIME && console.time("generateRivers");
+ Math.random = aleaPRNG(seed);
+ const {cells, features} = pack;
+
+ const riversData = {}; // rivers data
+ const riverParents = {};
+
+ const addCellToRiver = function (cell, river) {
+ if (!riversData[river]) riversData[river] = [cell];
+ else riversData[river].push(cell);
+ };
+
+ const newCells = {
+ ...cells,
+ fl: new Uint16Array(cells.i.length), // water flux array
+ r: new Uint16Array(cells.i.length), // rivers array
+ conf: new Uint8Array(cells.i.length) // confluences array
+ };
+
+ let riverNext = 1; // first river id is 1
+
+ const h = alterHeights(pack, utils);
+ Lakes.detectCloseLakes(h);
+ const resolvedH = resolveDepressions(pack, config, utils, h);
+ const {updatedCells, updatedFeatures, updatedRivers} = drainWater(pack, grid, config, utils, modules, newCells, resolvedH, riversData, riverParents, riverNext);
+ const {finalCells, finalRivers} = defineRivers(pack, config, utils, updatedCells, riversData, riverParents);
+
+ calculateConfluenceFlux(finalCells, resolvedH);
+ Lakes.cleanupLakeData();
+
+ let finalH = resolvedH;
+ if (allowErosion) {
+ finalH = Uint8Array.from(resolvedH); // apply gradient
+ downcutRivers(pack, finalCells, finalH); // downcut river beds
+ }
+
+ TIME && console.timeEnd("generateRivers");
+
+ return {
+ pack: {
+ ...pack,
+ cells: {
+ ...pack.cells,
+ ...finalCells,
+ h: finalH
+ },
+ features: updatedFeatures,
+ rivers: finalRivers
+ }
+ };
+
+ function drainWater(pack, grid, config, utils, modules, cells, h, riversData, riverParents, riverNext) {
+ const {cellsCount} = config;
+ const {Lakes} = modules;
+ const MIN_FLUX_TO_FORM_RIVER = 30;
+ const cellsNumberModifier = (cellsCount / 10000) ** 0.25;
+
+ const prec = grid.cells.prec;
+ const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
+ const lakeOutCells = Lakes.defineClimateData(h);
+
+ land.forEach(function (i) {
+ cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
+
+ // create lake outlet if lake is not in deep depression and flux > evaporation
+ const lakes = lakeOutCells[i]
+ ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
+ : [];
+ for (const lake of lakes) {
+ const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
+ cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
+
+ // allow chain lakes to retain identity
+ if (cells.r[lakeCell] !== lake.river) {
+ const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
+
+ if (sameRiver) {
+ cells.r[lakeCell] = lake.river;
+ addCellToRiver(lakeCell, lake.river);
+ } else {
+ cells.r[lakeCell] = riverNext;
+ addCellToRiver(lakeCell, riverNext);
+ riverNext++;
+ }
+ }
+
+ lake.outlet = cells.r[lakeCell];
+ flowDown(i, cells.fl[lakeCell], lake.outlet);
+ }
+
+ // assign all tributary rivers to outlet basin
+ const outlet = lakes[0]?.outlet;
+ for (const lake of lakes) {
+ if (!Array.isArray(lake.inlets)) continue;
+ for (const inlet of lake.inlets) {
+ riverParents[inlet] = outlet;
+ }
+ }
+
+ // near-border cell: pour water out of the screen
+ if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
+
+ // downhill cell (make sure it's not in the source lake)
+ let min = null;
+ if (lakeOutCells[i]) {
+ const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
+ min = filtered.sort((a, b) => h[a] - h[b])[0];
+ } else if (cells.haven[i]) {
+ min = cells.haven[i];
+ } else {
+ min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
+ }
+
+ // cells is depressed
+ if (h[i] <= h[min]) return;
+
+ if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
+ // flux is too small to operate as a river
+ if (h[min] >= 20) cells.fl[min] += cells.fl[i];
+ return;
+ }
+
+ // proclaim a new river
+ if (!cells.r[i]) {
+ cells.r[i] = riverNext;
+ addCellToRiver(i, riverNext);
+ riverNext++;
+ }
+
+ flowDown(min, cells.fl[i], cells.r[i]);
+ });
+
+ function flowDown(toCell, fromFlux, river) {
+ const toFlux = cells.fl[toCell] - cells.conf[toCell];
+ const toRiver = cells.r[toCell];
+
+ if (toRiver) {
+ // downhill cell already has river assigned
+ if (fromFlux > toFlux) {
+ cells.conf[toCell] += cells.fl[toCell]; // mark confluence
+ if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
+ cells.r[toCell] = river; // re-assign river if downhill part has less flux
+ } else {
+ cells.conf[toCell] += fromFlux; // mark confluence
+ if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
+ }
+ } else cells.r[toCell] = river; // assign the river to the downhill cell
+
+ if (h[toCell] < 20) {
+ // pour water to the water body
+ const waterBody = features[cells.f[toCell]];
+ if (waterBody.type === "lake") {
+ if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
+ waterBody.river = river;
+ waterBody.enteringFlux = fromFlux;
+ }
+ waterBody.flux = waterBody.flux + fromFlux;
+ if (!waterBody.inlets) waterBody.inlets = [river];
+ else waterBody.inlets.push(river);
+ }
+ } else {
+ // propagate flux and add next river segment
+ cells.fl[toCell] += fromFlux;
+ }
+
+ addCellToRiver(toCell, river);
+ }
+
+ return {
+ updatedCells: cells,
+ updatedFeatures: features,
+ updatedRivers: []
+ };
+ }
+
+ function defineRivers(pack, config, utils, cells, riversData, riverParents) {
+ const {cellsCount} = config;
+ const {rn} = utils;
+
+ // re-initialize rivers and confluence arrays
+ const newCells = {
+ ...cells,
+ r: new Uint16Array(cells.i.length),
+ conf: new Uint16Array(cells.i.length)
+ };
+ const rivers = [];
+
+ const defaultWidthFactor = rn(1 / (cellsCount / 10000) ** 0.25, 2);
+ const mainStemWidthFactor = defaultWidthFactor * 1.2;
+
+ for (const key in riversData) {
+ const riverCells = riversData[key];
+ if (riverCells.length < 3) continue; // exclude tiny rivers
+
+ const riverId = +key;
+ for (const cell of riverCells) {
+ if (cell < 0 || cells.h[cell] < 20) continue;
+
+ // mark real confluences and assign river to cells
+ if (newCells.r[cell]) newCells.conf[cell] = 1;
+ else newCells.r[cell] = riverId;
+ }
+
+ const source = riverCells[0];
+ const mouth = riverCells[riverCells.length - 2];
+ const parent = riverParents[key] || 0;
+
+ const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
+ const meanderedPoints = addMeandering(pack, utils, riverCells);
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(utils, meanderedPoints);
+ const sourceWidth = getSourceWidth(utils, cells.fl[source]);
+ const width = getWidth(utils,
+ getOffset(utils, {
+ flux: discharge,
+ pointIndex: meanderedPoints.length,
+ widthFactor,
+ startingWidth: sourceWidth
+ })
+ );
+
+ rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth,
+ parent,
+ cells: riverCells
+ });
+ }
+
+ return {
+ finalCells: newCells,
+ finalRivers: rivers
+ };
+ }
+
+ function downcutRivers(pack, cells, h) {
+ const MAX_DOWNCUT = 5;
+
+ for (const i of pack.cells.i) {
+ if (cells.h[i] < 35) continue; // don't donwcut lowlands
+ if (!cells.fl[i]) continue;
+
+ const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
+ const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
+ if (!higherFlux) continue;
+
+ const downcut = Math.floor(cells.fl[i] / higherFlux);
+ if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
+ }
+ }
+
+ function calculateConfluenceFlux(cells, h) {
+ for (const i of cells.i) {
+ if (!cells.conf[i]) continue;
+
+ const sortedInflux = cells.c[i]
+ .filter(c => cells.r[c] && h[c] > h[i])
+ .map(c => cells.fl[c])
+ .sort((a, b) => b - a);
+ cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
+ }
+ }
+};
+
+// add distance to water value to land cells to make map less depressed
+export const alterHeights = (pack, utils) => {
+ const {d3} = utils;
+ const {h, c, t} = pack.cells;
+ return Array.from(h).map((h, i) => {
+ if (h < 20 || t[i] < 1) return h;
+ return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
+ });
+};
+
+// depression filling algorithm (for a correct water flux modeling)
+export const resolveDepressions = function (pack, config, utils, h) {
+ const {resolveDepressionsSteps, WARN} = config;
+ const {d3} = utils;
+ const {cells, features} = pack;
+ const maxIterations = resolveDepressionsSteps;
+ const checkLakeMaxIteration = maxIterations * 0.85;
+ const elevateLakeMaxIteration = maxIterations * 0.75;
+
+ const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
+
+ const lakes = features.filter(f => f.type === "lake");
+ const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
+ land.sort((a, b) => h[a] - h[b]); // lowest cells go first
+
+ const progress = [];
+ let depressions = Infinity;
+ let prevDepressions = null;
+ for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
+ if (progress.length > 5 && d3.sum(progress) > 0) {
+ // bad progress, abort and set heights back
+ h = alterHeights(pack, utils);
+ depressions = progress[0];
+ break;
+ }
+
+ depressions = 0;
+
+ if (iteration < checkLakeMaxIteration) {
+ for (const l of lakes) {
+ if (l.closed) continue;
+ const minHeight = d3.min(l.shoreline.map(s => h[s]));
+ if (minHeight >= 100 || l.height > minHeight) continue;
+
+ if (iteration > elevateLakeMaxIteration) {
+ l.shoreline.forEach(i => (h[i] = cells.h[i]));
+ l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
+ l.closed = true;
+ continue;
+ }
+
+ depressions++;
+ l.height = minHeight + 0.2;
+ }
+ }
+
+ for (const i of land) {
+ const minHeight = d3.min(cells.c[i].map(c => height(c)));
+ if (minHeight >= 100 || h[i] > minHeight) continue;
+
+ depressions++;
+ h[i] = minHeight + 0.1;
+ }
+
+ prevDepressions !== null && progress.push(depressions - prevDepressions);
+ prevDepressions = depressions;
+ }
+
+ depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
+ return h;
+};
+
+// add points at 1/3 and 2/3 of a line between adjacents river cells
+export const addMeandering = function (pack, utils, riverCells, riverPoints = null, meandering = 0.5) {
+ const {fl, h} = pack.cells;
+ const meandered = [];
+ const lastStep = riverCells.length - 1;
+ const points = getRiverPoints(pack, riverCells, riverPoints);
+ let step = h[riverCells[0]] < 20 ? 1 : 10;
+
+ for (let i = 0; i <= lastStep; i++, step++) {
+ const cell = riverCells[i];
+ const isLastCell = i === lastStep;
+
+ const [x1, y1] = points[i];
+
+ meandered.push([x1, y1, fl[cell]]);
+ if (isLastCell) break;
+
+ const nextCell = riverCells[i + 1];
+ const [x2, y2] = points[i + 1];
+
+ if (nextCell === -1) {
+ meandered.push([x2, y2, fl[cell]]);
+ break;
+ }
+
+ const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
+ if (dist2 <= 25 && riverCells.length >= 6) continue;
+
+ const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ const sinMeander = Math.sin(angle) * meander;
+ const cosMeander = Math.cos(angle) * meander;
+
+ if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
+ // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
+ const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
+ const p1y = (y1 * 2 + y2) / 3 + cosMeander;
+ const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
+ const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
+ meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
+ } else if (dist2 > 25 || riverCells.length < 6) {
+ // if dist is medium or river is small add 1 extra middlepoint
+ const p1x = (x1 + x2) / 2 + -sinMeander;
+ const p1y = (y1 + y2) / 2 + cosMeander;
+ meandered.push([p1x, p1y, 0]);
+ }
+ }
+
+ return meandered;
+};
+
+export const getRiverPoints = (pack, riverCells, riverPoints) => {
+ if (riverPoints) return riverPoints;
+
+ const {p} = pack.cells;
+ return riverCells.map((cell, i) => {
+ if (cell === -1) return getBorderPoint(pack, riverCells[i - 1]);
+ return p[cell];
+ });
+};
+
+export const getBorderPoint = (pack, config, i) => {
+ const {graphWidth, graphHeight} = config;
+ const [x, y] = pack.cells.p[i];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) return [x, 0];
+ else if (min === graphHeight - y) return [x, graphHeight];
+ else if (min === x) return [0, y];
+ return [graphWidth, y];
+};
+
+const FLUX_FACTOR = 500;
+const MAX_FLUX_WIDTH = 1;
+const LENGTH_FACTOR = 200;
+const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
+const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+
+export const getOffset = (utils, {flux, pointIndex, widthFactor, startingWidth}) => {
+ if (pointIndex === 0) return startingWidth;
+
+ const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || LENGTH_PROGRESSION.at(-1));
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+};
+
+export const getSourceWidth = (utils, flux) => {
+ const {rn} = utils;
+ return rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
+};
+
+// build polygon from a list of points and calculated offset (width)
+export const getRiverPath = (utils, points, widthFactor, startingWidth) => {
+ const {lineGen, d3, round} = utils;
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const riverPointsLeft = [];
+ const riverPointsRight = [];
+ let flux = 0;
+
+ for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
+ const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
+ const [x1, y1, pointFlux] = points[pointIndex];
+ const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
+ if (pointFlux > flux) flux = pointFlux;
+
+ const offset = getOffset(utils, {flux, pointIndex, widthFactor, startingWidth});
+ const angle = Math.atan2(y0 - y2, x0 - x2);
+ const sinOffset = Math.sin(angle) * offset;
+ const cosOffset = Math.cos(angle) * offset;
+
+ riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
+ riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
+ }
+
+ const right = lineGen(riverPointsRight.reverse());
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+
+ return round(right + left, 1);
+};
+
+export const specify = function (pack, modules, utils) {
+ const rivers = pack.rivers;
+ if (!rivers.length) return pack;
+
+ const updatedRivers = rivers.map(river => ({
+ ...river,
+ basin: getBasin(pack, river.i),
+ name: getName(pack, modules, river.mouth),
+ type: getType(pack, utils, river)
+ }));
+
+ return {
+ ...pack,
+ rivers: updatedRivers
+ };
+};
+
+export const getName = function (pack, modules, cell) {
+ const {Names} = modules;
+ return Names.getCulture(pack.cells.culture[cell]);
+};
+
+// weighted arrays of river type names
+const riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+};
+
+let smallLength = null;
+export const getType = function (pack, utils, {i, length, parent}) {
+ const {rw, each} = utils;
+ if (smallLength === null) {
+ const threshold = Math.ceil(pack.rivers.length * 0.15);
+ smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
+ }
+
+ const isSmall = length < smallLength;
+ const isFork = each(3)(i) && parent && parent !== i;
+ return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+};
+
+export const getApproximateLength = (utils, points) => {
+ const {rn} = utils;
+ const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+ return rn(length, 2);
+};
+
+// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
+// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
+export const getWidth = (utils, offset) => {
+ const {rn} = utils;
+ return rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+};
+
+// remove river and all its tributaries
+export const remove = function (pack, grid, id) {
+ const cells = pack.cells;
+ const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
+
+ // Update cells data
+ cells.r.forEach((r, i) => {
+ if (!r || !riversToRemove.includes(r)) return;
+ cells.r[i] = 0;
+ cells.fl[i] = grid.cells.prec[cells.g[i]];
+ cells.conf[i] = 0;
+ });
+
+ const updatedRivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+
+ return {
+ ...pack,
+ rivers: updatedRivers
+ };
+};
+
+export const getBasin = function (pack, r) {
+ const parent = pack.rivers.find(river => river.i === r)?.parent;
+ if (!parent || r === parent) return r;
+ return getBasin(pack, parent);
+};
+
+export const getNextId = function (rivers) {
+ return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
+};
\ No newline at end of file
diff --git a/procedural/src/engine/modules/routes-generator.js b/procedural/src/engine/modules/routes-generator.js
new file mode 100644
index 00000000..71337176
--- /dev/null
+++ b/procedural/src/engine/modules/routes-generator.js
@@ -0,0 +1,712 @@
+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
+};
+
+export function generate(pack, grid, utils, lockedRoutes = []) {
+ const { dist2, findPath, findCell, rn } = utils;
+ 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();
+
+ const routes = createRoutesData(lockedRoutes);
+ const cellRoutes = buildLinks(routes);
+
+ return {
+ routes,
+ cellRoutes
+ };
+
+ function sortBurgsByFeature(burgs) {
+ const burgsByFeature = {};
+ const capitalsByFeature = {};
+ const portsByFeature = {};
+
+ const addBurg = (object, feature, burg) => {
+ if (!object[feature]) object[feature] = [];
+ object[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() {
+ 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 });
+ }
+ });
+ }
+
+ return mainRoads;
+ }
+
+ function 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 });
+ }
+ });
+ }
+
+ return trails;
+ }
+
+ function 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 });
+ }
+ });
+ }
+
+ 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 = utils.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 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 } = utils.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;
+ }
+}
+
+export 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;
+}
+
+// connect cell with routes system by land
+export function connect(cellId, pack, utils) {
+ const { findPath } = utils;
+ const getCost = createCostEvaluator({ isWater: false, connections: new Map() });
+ const pathCells = findPath(cellId, isConnected, getCost);
+ if (!pathCells) return null;
+
+ const pointsArray = preparePointsArray();
+ const points = getPoints("trails", pathCells, pointsArray);
+ const feature = pack.cells.f[cellId];
+ const routeId = getNextId(pack.routes);
+ const newRoute = { i: routeId, group: "trails", feature, points };
+
+ const connections = [];
+ for (let i = 0; i < pathCells.length; i++) {
+ const from = pathCells[i];
+ const to = pathCells[i + 1];
+ if (to) connections.push({ from, to, routeId });
+ }
+
+ return { route: newRoute, connections };
+
+ function createCostEvaluator({ isWater, connections }) {
+ const { dist2 } = utils;
+ return isWater ? getWaterPathCost : getLandPathCost;
+
+ function getLandPathCost(current, next) {
+ if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
+
+ const habitability = utils.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 (utils.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 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 { rn, findCell } = utils;
+ 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 isConnected(cellId) {
+ const routes = pack.cells.routes;
+ return routes[cellId] && Object.keys(routes[cellId]).length > 0;
+ }
+}
+
+// utility functions
+export function isConnected(cellId, pack) {
+ const routes = pack.cells.routes;
+ return routes[cellId] && Object.keys(routes[cellId]).length > 0;
+}
+
+export function areConnected(from, to, pack) {
+ const routeId = pack.cells.routes[from]?.[to];
+ return routeId !== undefined;
+}
+
+export function getRoute(from, to, pack) {
+ 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;
+}
+
+export function hasRoad(cellId, pack) {
+ 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";
+ });
+}
+
+export function isCrossroad(cellId, pack) {
+ 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;
+}
+
+// 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 }
+};
+
+export function generateName({ group, points }, pack, utils) {
+ const { ra, rw, getAdjective } = utils;
+
+ 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;
+ }
+}
+
+export function getNextId(routes) {
+ return routes.length ? Math.max(...routes.map(r => r.i)) + 1 : 0;
+}
+
+export function remove(route, pack) {
+ const routes = pack.cells.routes;
+ const removedConnections = [];
+
+ 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) {
+ removedConnections.push({ from, to });
+ }
+ }
+ }
+
+ const updatedRoutes = pack.routes.filter(r => r.i !== route.i);
+ const updatedCellRoutes = { ...routes };
+
+ removedConnections.forEach(({ from, to }) => {
+ if (updatedCellRoutes[from]) delete updatedCellRoutes[from][to];
+ if (updatedCellRoutes[to]) delete updatedCellRoutes[to][from];
+ });
+
+ return {
+ routes: updatedRoutes,
+ cellRoutes: updatedCellRoutes,
+ removedConnections
+ };
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/submap.js b/procedural/src/engine/modules/submap.js
new file mode 100644
index 00000000..6e79556b
--- /dev/null
+++ b/procedural/src/engine/modules/submap.js
@@ -0,0 +1,375 @@
+"use strict";
+
+/*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+*/
+export function process({projection, inverse, scale}, grid, pack, notes, config, utils) {
+ const {
+ deepCopy, generateGrid, Features, addLakesInDeepDepressions, openNearSeaLakes,
+ OceanLayers, calculateMapCoordinates, calculateTemperatures, reGraph,
+ createDefaultRuler, Rivers, BurgsAndStates, Routes, Provinces, Markers,
+ isWater, findCell, findAll, rn, unique, d3, lineclip, getPolesOfInaccessibility,
+ WARN
+ } = utils;
+
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(parentMap.pack.rivers, Rivers);
+
+ const newGrid = generateGrid();
+ const newPack = {};
+ const newNotes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale, newGrid, d3, isWater);
+
+ Features.markupGrid(newGrid);
+ addLakesInDeepDepressions(newGrid);
+ openNearSeaLakes(newGrid);
+
+ OceanLayers(newGrid);
+ calculateMapCoordinates(newGrid);
+ calculateTemperatures(newGrid);
+
+ reGraph(newGrid, newPack);
+ Features.markupPack(newPack);
+ createDefaultRuler(newPack);
+
+ restoreCellData(parentMap, inverse, scale, newPack, d3, isWater, groupCellsByType);
+ restoreRivers(riversData, projection, scale, newPack, isInMap, findCell, rn, Rivers, config);
+ restoreCultures(parentMap, projection, newPack, getPolesOfInaccessibility, findCell, rn, isInMap, config);
+ restoreBurgs(parentMap, projection, scale, newPack, d3, groupCellsByType, findCell, isWater, isInMap, rn, BurgsAndStates, WARN, config);
+ restoreStates(parentMap, projection, newPack, BurgsAndStates, findCell, isInMap, rn, config);
+ restoreRoutes(parentMap, projection, newPack, isInMap, rn, findCell, lineclip, Routes, config);
+ restoreReligions(parentMap, projection, newPack, getPolesOfInaccessibility, findCell, rn, isInMap, config);
+ restoreProvinces(parentMap, newPack, Provinces, findCell);
+ restoreFeatureDetails(parentMap, inverse, newPack);
+ restoreMarkers(parentMap, projection, newPack, isInMap, findCell, rn, Markers);
+ restoreZones(parentMap, projection, scale, newPack, isInMap, findCell, findAll, unique);
+
+ return {
+ grid: newGrid,
+ pack: newPack,
+ notes: newNotes
+ };
+}
+
+function resamplePrimaryGridData(parentMap, inverse, scale, grid, d3, isWater) {
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap(grid, d3, isWater);
+}
+
+function smoothHeightmap(grid, d3, isWater) {
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+}
+
+function restoreCellData(parentMap, inverse, scale, pack, d3, isWater, groupCellsByType) {
+ pack.cells.biome = new Uint8Array(pack.cells.i.length);
+ pack.cells.fl = new Uint16Array(pack.cells.i.length);
+ pack.cells.s = new Int16Array(pack.cells.i.length);
+ pack.cells.pop = new Float32Array(pack.cells.i.length);
+ pack.cells.culture = new Uint16Array(pack.cells.i.length);
+ pack.cells.state = new Uint16Array(pack.cells.i.length);
+ pack.cells.burg = new Uint16Array(pack.cells.i.length);
+ pack.cells.religion = new Uint16Array(pack.cells.i.length);
+ pack.cells.province = new Uint16Array(pack.cells.i.length);
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+}
+
+function saveRiversData(parentRivers, Rivers) {
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+}
+
+function restoreRivers(riversData, projection, scale, pack, isInMap, findCell, rn, Rivers, config) {
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ pack.rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ pack.rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+}
+
+function restoreCultures(parentMap, projection, pack, getPolesOfInaccessibility, findCell, rn, isInMap, config) {
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+ pack.cultures = parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y, config) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+}
+
+function restoreBurgs(parentMap, projection, scale, pack, d3, groupCellsByType, findCell, isWater, isInMap, rn, BurgsAndStates, WARN, config) {
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ pack.burgs = parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp, config)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, BurgsAndStates, rn);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, BurgsAndStates, rn) {
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+}
+
+function restoreStates(parentMap, projection, pack, BurgsAndStates, findCell, isInMap, rn, config) {
+ const validStates = new Set(pack.cells.state);
+ pack.states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ pack.states = pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords, config) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos, config) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos, config)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase, config) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+}
+
+function restoreRoutes(parentMap, projection, pack, isInMap, rn, findCell, lineclip, Routes, config) {
+ pack.routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, config.graphWidth, config.graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(pack.routes);
+}
+
+function restoreReligions(parentMap, projection, pack, getPolesOfInaccessibility, findCell, rn, isInMap, config) {
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ pack.religions = parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y, config) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+}
+
+function restoreProvinces(parentMap, pack, Provinces, findCell) {
+ const validProvinces = new Set(pack.cells.province);
+ pack.provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+}
+
+function restoreMarkers(parentMap, projection, pack, isInMap, findCell, rn, Markers) {
+ pack.markers = parentMap.pack.markers;
+ pack.markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+}
+
+function restoreZones(parentMap, projection, scale, pack, isInMap, findCell, findAll, unique) {
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ pack.zones = parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+}
+
+function restoreFeatureDetails(parentMap, inverse, pack) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+}
+
+function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+}
+
+function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+}
+
+function isInMap(x, y, config) {
+ return x >= 0 && x <= config.graphWidth && y >= 0 && y <= config.graphHeight;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/voronoi.js b/procedural/src/engine/modules/voronoi.js
new file mode 100644
index 00000000..d73bbb8e
--- /dev/null
+++ b/procedural/src/engine/modules/voronoi.js
@@ -0,0 +1,135 @@
+export class Voronoi {
+ /**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+ constructor(delaunay, points, pointsN) {
+ this.delaunay = delaunay;
+ this.points = points;
+ this.pointsN = pointsN;
+ this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
+ this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+
+ // Half-edges are the indices into the delaunator outputs:
+ // delaunay.triangles[e] gives the point ID where the half-edge starts
+ // delaunay.halfedges[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
+ for (let e = 0; e < this.delaunay.triangles.length; e++) {
+
+ const p = this.delaunay.triangles[this.nextHalfedge(e)];
+ if (p < this.pointsN && !this.cells.c[p]) {
+ const edges = this.edgesAroundPoint(e);
+ this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
+ this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
+ this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
+ }
+
+ const t = this.triangleOfEdge(e);
+ if (!this.vertices.p[t]) {
+ this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
+ this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
+ this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
+ }
+ }
+ }
+
+ /**
+ * Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The IDs of the points comprising the given triangle.
+ */
+ pointsOfTriangle(t) {
+ return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ }
+
+ /**
+ * Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {number[]} The indices of the triangles that share half-edges with this triangle.
+ */
+ trianglesAdjacentToTriangle(t) {
+ let triangles = [];
+ for (let edge of this.edgesOfTriangle(t)) {
+ let opposite = this.delaunay.halfedges[edge];
+ triangles.push(this.triangleOfEdge(opposite));
+ }
+ return triangles;
+ }
+
+ /**
+ * Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
+ * @param {number} start The index of an incoming half-edge that leads to the desired point
+ * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ */
+ edgesAroundPoint(start) {
+ const result = [];
+ let incoming = start;
+ do {
+ result.push(incoming);
+ const outgoing = this.nextHalfedge(incoming);
+ incoming = this.delaunay.halfedges[outgoing];
+ } while (incoming !== -1 && incoming !== start && result.length < 20);
+ return result;
+ }
+
+ /**
+ * Returns the center of the triangle located at the given index.
+ * @param {number} t The index of the triangle
+ * @returns {[number, number]}
+ */
+ triangleCenter(t) {
+ let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ return this.circumcenter(vertices[0], vertices[1], vertices[2]);
+ }
+
+ /**
+ * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The edges of the triangle.
+ */
+ edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+
+ /**
+ * Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} e The index of the edge
+ * @returns {number} The index of the triangle
+ */
+ triangleOfEdge(e) { return Math.floor(e / 3); }
+
+ /**
+ * Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the next half edge
+ */
+ nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+
+ /**
+ * Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the previous half edge
+ */
+ prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+
+ /**
+ * Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
+ * @param {[number, number]} a The coordinates of the first point of the triangle
+ * @param {[number, number]} b The coordinates of the second point of the triangle
+ * @param {[number, number]} c The coordinates of the third point of the triangle
+ * @return {[number, number]} The coordinates of the circumcenter of the triangle.
+ */
+ circumcenter(a, b, c) {
+ const [ax, ay] = a;
+ const [bx, by] = b;
+ const [cx, cy] = c;
+ const ad = ax * ax + ay * ay;
+ const bd = bx * bx + by * by;
+ const cd = cx * cx + cy * cy;
+ const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
+ return [
+ Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
+ Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
+ ];
+ }
+}
\ No newline at end of file
diff --git a/procedural/src/engine/modules/zones-generator.js b/procedural/src/engine/modules/zones-generator.js
new file mode 100644
index 00000000..bb763e6c
--- /dev/null
+++ b/procedural/src/engine/modules/zones-generator.js
@@ -0,0 +1,485 @@
+"use strict";
+
+const zoneConfig = {
+ 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
+};
+
+export function generate(pack, notes, utils, config = {}) {
+ const {gauss, ra, rw, P, rand, getAdjective} = utils.random;
+ const {Names, Routes, FlatQueue, d3} = utils;
+ const {TIME = false} = config;
+
+ TIME && console.time("generateZones");
+
+ const globalModifier = config.globalModifier || 1;
+ const usedCells = new Uint8Array(pack.cells.i.length);
+ const zones = [];
+
+ Object.values(zoneConfig).forEach(type => {
+ const expectedNumber = type.quantity * globalModifier;
+ let number = gauss(expectedNumber, expectedNumber / 2, 0, 100);
+ while (number--) {
+ const zone = type.generate(pack, usedCells, utils);
+ if (zone) zones.push({...zone, i: zones.length});
+ }
+ });
+
+ TIME && console.timeEnd("generateZones");
+
+ return zones;
+}
+
+function addInvasion(pack, usedCells, utils) {
+ const {ra, rand, rw, P} = utils.random;
+ const {getAdjective} = utils.random;
+ 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 null;
+ 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 null;
+
+ 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;
+
+ return {name, type: "Invasion", cells: invasionCells, color: "url(#hatch1)"};
+}
+
+function addRebels(pack, usedCells, utils) {
+ const {ra, rand, rw} = utils.random;
+ const {getAdjective} = utils.random;
+ const {cells, states} = pack;
+
+ const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(Boolean)));
+ if (!state) return null;
+
+ const neibStateId = ra(state.neighbors.filter(n => n && !states[n].removed));
+ if (!neibStateId) return null;
+
+ 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;
+ return {name, type: "Rebels", cells: cellsArray, color: "url(#hatch3)"};
+}
+
+function addProselytism(pack, usedCells, utils) {
+ const {ra, rand} = utils.random;
+ const {getAdjective} = utils.random;
+ const {cells, religions} = pack;
+
+ const organizedReligions = religions.filter(r => r.i && !r.removed && r.type === "Organized");
+ const religion = ra(organizedReligions);
+ if (!religion) return null;
+
+ 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 null;
+
+ 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[neibCellId]) return;
+ usedCells[neibCellId] = 1;
+ queue.push(neibCellId);
+ });
+ }
+
+ const name = `${getAdjective(religion.name.split(" ")[0])} Proselytism`;
+ return {name, type: "Proselytism", cells: proselytismCells, color: "url(#hatch6)"};
+}
+
+function addCrusade(pack, usedCells, utils) {
+ const {ra} = utils.random;
+ const {getAdjective} = utils.random;
+ const {cells, religions} = pack;
+
+ const heresies = religions.filter(r => !r.removed && r.type === "Heresy");
+ if (!heresies.length) return null;
+
+ const heresy = ra(heresies);
+ const crusadeCells = cells.i.filter(i => !usedCells[i] && cells.religion[i] === heresy.i);
+ if (!crusadeCells.length) return null;
+ crusadeCells.forEach(i => (usedCells[i] = 1));
+
+ const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade";
+ return {
+ name,
+ type: "Crusade",
+ cells: Array.from(crusadeCells),
+ color: "url(#hatch6)"
+ };
+}
+
+function addDisease(pack, usedCells, utils) {
+ const {ra, rand, rw} = utils.random;
+ const {FlatQueue, Routes} = utils;
+ const {cells, burgs} = pack;
+
+ const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); // random burg
+ if (!burg) return null;
+
+ 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})}`;
+
+ return {name, type: "Disease", cells: cellsArray, color: "url(#hatch12)"};
+}
+
+function addDisaster(pack, usedCells, utils) {
+ const {ra, rand, rw} = utils.random;
+ const {getAdjective} = utils.random;
+ const {FlatQueue} = utils;
+ const {cells, burgs} = pack;
+
+ const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed));
+ if (!burg) return null;
+ 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;
+ return {name, type: "Disaster", cells: cellsArray, color: "url(#hatch5)"};
+}
+
+function addEruption(pack, usedCells, utils, notes) {
+ const {ra, rand, P} = utils.random;
+ const {cells, markers} = pack;
+
+ const volcanoe = markers.find(m => m.type === "volcanoes" && !usedCells[m.cell]);
+ if (!volcanoe) return null;
+ 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);
+ });
+ }
+
+ return {name, type: "Eruption", cells: cellsArray, color: "url(#hatch7)"};
+}
+
+function addAvalanche(pack, usedCells, utils) {
+ const {ra, rand, P} = utils.random;
+ const {getAdjective} = utils.random;
+ const {Routes, Names} = utils;
+ const {cells} = pack;
+
+ const routeCells = cells.i.filter(i => !usedCells[i] && Routes.isConnected(i) && cells.h[i] >= 70);
+ if (!routeCells.length) return null;
+
+ 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";
+ return {name, type: "Avalanche", cells: cellsArray, color: "url(#hatch5)"};
+}
+
+function addFault(pack, usedCells, utils) {
+ const {ra, rand} = utils.random;
+ const {getAdjective} = utils.random;
+ const {Names} = utils;
+ const cells = pack.cells;
+
+ const elevatedCells = cells.i.filter(i => !usedCells[i] && cells.h[i] > 50 && cells.h[i] < 70);
+ if (!elevatedCells.length) return null;
+
+ 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";
+ return {name, type: "Fault", cells: cellsArray, color: "url(#hatch2)"};
+}
+
+function addFlood(pack, usedCells, utils) {
+ const {ra, rand} = utils.random;
+ const {getAdjective} = utils.random;
+ const {d3} = utils;
+ 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 null;
+
+ 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";
+ return {name, type: "Flood", cells: cellsArray, color: "url(#hatch13)"};
+}
+
+function addTsunami(pack, usedCells, utils) {
+ const {ra, rand} = utils.random;
+ const {getAdjective} = utils.random;
+ const {Names} = utils;
+ 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 null;
+
+ 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";
+ return {name, type: "Tsunami", cells: cellsArray, color: "url(#hatch13)"};
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/biomes.txt b/procedural/src/engine/support/biomes.txt
new file mode 100644
index 00000000..aa018c21
--- /dev/null
+++ b/procedural/src/engine/support/biomes.txt
@@ -0,0 +1,129 @@
+"use strict";
+
+const MIN_LAND_HEIGHT = 20;
+
+export 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, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 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: Array.from({length: name.length}, (_, i) => i), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
+};
+
+// assign biome id for each cell
+export function define(pack, grid, config, utils) {
+ const {TIME, d3, rn} = utils;
+ TIME && console.time("defineBiomes");
+
+ const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
+ const {temp, prec} = grid.cells;
+ const biome = new Uint8Array(pack.cells.i.length); // biomes array
+ const biomesData = getDefault();
+
+ 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]];
+ biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId]), biomesData);
+ }
+
+ 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");
+ return {biome};
+}
+
+export function getId(moisture, temperature, height, hasRiver, biomesData = null) {
+ const data = biomesData || getDefault();
+
+ 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 data.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;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/biomes_config.md b/procedural/src/engine/support/biomes_config.md
new file mode 100644
index 00000000..bde6345c
--- /dev/null
+++ b/procedural/src/engine/support/biomes_config.md
@@ -0,0 +1,28 @@
+# Biomes Module - Configuration Properties
+
+The refactored `biomes.js` module does not require any configuration properties from DOM elements.
+
+## Analysis
+
+After careful examination of the original `biomes.js` code, no instances of the following patterns were found:
+
+- `byId()` calls to read DOM values
+- Direct DOM element access
+- Configuration parameters read from UI elements
+
+## Config Object
+
+The `config` parameter is included in the function signature for consistency with the refactoring pattern, but no properties are currently needed:
+
+```javascript
+export function define(pack, grid, config, utils) {
+ // config parameter available but not used in this module
+}
+```
+
+## Notes
+
+- The biomes module operates purely on data structures (`pack` and `grid`)
+- All configuration is embedded within the module's default data
+- No external configuration parameters are required
+- The module is self-contained regarding biome generation logic
\ No newline at end of file
diff --git a/procedural/src/engine/support/biomes_external.md b/procedural/src/engine/support/biomes_external.md
new file mode 100644
index 00000000..d28791e4
--- /dev/null
+++ b/procedural/src/engine/support/biomes_external.md
@@ -0,0 +1,33 @@
+# Biomes Module - External Dependencies
+
+The refactored `biomes.js` module requires the following external dependencies to be injected via the `utils` parameter:
+
+## Required Utilities
+
+- **`TIME`** - Global timing flag for performance monitoring (boolean)
+- **`d3`** - D3.js library for mathematical functions
+ - `d3.mean()` - Used for calculating average moisture values
+- **`rn`** - Rounding utility function for numerical precision
+
+## Import Structure
+
+When integrating this module, the calling code should provide these utilities:
+
+```javascript
+import { define, getId, getDefault } from './biomes.js';
+
+const utils = {
+ TIME: globalTimeFlag,
+ d3: d3Library,
+ rn: roundingFunction
+};
+
+// Usage
+const result = define(pack, grid, config, utils);
+```
+
+## Notes
+
+- No additional external modules need to be imported by the biomes module itself
+- All dependencies are injected rather than directly imported
+- The module maintains compatibility with the original d3.range functionality by using `Array.from()` instead
\ No newline at end of file
diff --git a/procedural/src/engine/support/biomes_prompt.md b/procedural/src/engine/support/biomes_prompt.md
new file mode 100644
index 00000000..0a7db104
--- /dev/null
+++ b/procedural/src/engine/support/biomes_prompt.md
@@ -0,0 +1,209 @@
+# biomes.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.txt`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `biomes.js`.
+
+**File Content:**
+```javascript
+"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};
+})();
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./biomes.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./biomes_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in biomes_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application.
diff --git a/procedural/src/engine/support/biomes_render.md b/procedural/src/engine/support/biomes_render.md
new file mode 100644
index 00000000..bd54a865
--- /dev/null
+++ b/procedural/src/engine/support/biomes_render.md
@@ -0,0 +1,9 @@
+# biomes.js render requirements
+
+After analyzing the original biomes.js code, no rendering or UI logic was found to remove. The module contains only:
+
+- Data structure definitions (biome names, colors, matrices)
+- Pure computational logic for biome assignment
+- Mathematical calculations for moisture and temperature
+
+The original module was already focused purely on data generation without any DOM manipulation, SVG rendering, or UI interactions.
diff --git a/procedural/src/engine/support/burgs-and-states.txt b/procedural/src/engine/support/burgs-and-states.txt
new file mode 100644
index 00000000..05cf3932
--- /dev/null
+++ b/procedural/src/engine/support/burgs-and-states.txt
@@ -0,0 +1,893 @@
+"use strict";
+
+export const generate = (pack, grid, config, utils) => {
+
+ const {cells, cultures} = pack;
+ const n = cells.i.length;
+
+ const newCells = {...cells, burg: new Uint16Array(n)};
+ const newPack = {...pack, cells: newCells};
+
+ const burgs = placeCapitals(newPack, grid, config, utils);
+ const states = createStates(newPack, burgs, config, utils);
+
+ placeTowns(newPack, burgs, grid, config, utils);
+ expandStates(newPack, grid, config, utils);
+ normalizeStates(newPack, utils);
+ getPoles(newPack, utils);
+
+ specifyBurgs(newPack, grid, utils);
+
+ collectStatistics(newPack);
+ assignColors(newPack, utils);
+
+ generateCampaigns(newPack, utils);
+ generateDiplomacy(newPack, utils);
+
+ return {
+ burgs: newPack.burgs,
+ states: newPack.states,
+ cells: {
+ ...pack.cells,
+ burg: newPack.cells.burg,
+ state: newPack.cells.state
+ }
+ };
+};
+
+function placeCapitals(pack, grid, config, utils) {
+ const {TIME, WARN, d3, graphWidth, graphHeight} = utils;
+ TIME && console.time("placeCapitals");
+ let count = config.statesNumber;
+ let burgs = [0];
+
+ const {cells} = pack;
+ const rand = () => 0.5 + Math.random() * 0.5;
+ const score = new Int16Array(cells.s.map(s => s * rand())); // cell score for capitals placement
+ const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ if (sorted.length < count * 10) {
+ count = Math.floor(sorted.length / 10);
+ if (!count) {
+ WARN && console.warn("There is no populated cells. Cannot generate states");
+ return burgs;
+ } else {
+ WARN && console.warn(`Not enough populated cells (${sorted.length}). Will generate only ${count} states`);
+ }
+ }
+
+ let burgsTree = d3.quadtree();
+ let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
+
+ for (let i = 0; burgs.length <= count; i++) {
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+
+ if (burgsTree.find(x, y, spacing) === undefined) {
+ burgs.push({cell, x, y});
+ burgsTree.add([x, y]);
+ }
+
+ if (i === sorted.length - 1) {
+ WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
+ burgsTree = d3.quadtree();
+ i = -1;
+ burgs = [0];
+ spacing /= 1.2;
+ }
+ }
+
+ burgs[0] = burgsTree;
+ TIME && console.timeEnd("placeCapitals");
+ return burgs;
+}
+
+// For each capital create a state
+function createStates(pack, burgs, config, utils) {
+ const {TIME, rn, each, Names, COA, getColors} = utils;
+ TIME && console.time("createStates");
+ const {cells, cultures} = pack;
+ const states = [{i: 0, name: "Neutrals"}];
+ const colors = getColors(burgs.length - 1);
+ const each5th = each(5);
+
+ burgs.forEach((b, i) => {
+ if (!i) return; // skip first element
+
+ // burgs data
+ b.i = b.state = i;
+ b.culture = cells.culture[b.cell];
+ b.name = Names.getCultureShort(b.culture);
+ b.feature = cells.f[b.cell];
+ b.capital = 1;
+
+ // states data
+ const expansionism = rn(Math.random() * config.sizeVariety + 1, 1);
+ const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
+ const name = Names.getState(basename, b.culture);
+ const type = cultures[b.culture].type;
+
+ const coa = COA.generate(null, null, null, type);
+ coa.shield = COA.getShield(b.culture, null);
+ states.push({
+ i,
+ color: colors[i - 1],
+ name,
+ expansionism,
+ capital: i,
+ type,
+ center: b.cell,
+ culture: b.culture,
+ coa
+ });
+ cells.burg[b.cell] = i;
+ });
+
+ TIME && console.timeEnd("createStates");
+ return states;
+}
+
+// place secondary settlements based on geo and economical evaluation
+function placeTowns(pack, burgs, grid, config, utils) {
+ const {TIME, ERROR, rn, gauss, Names, graphWidth, graphHeight} = utils;
+ TIME && console.time("placeTowns");
+ const {cells} = pack;
+ const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
+ const sorted = cells.i
+ .filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i])
+ .sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ const desiredNumber =
+ config.manorsInput == 1000
+ ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8)
+ : config.manorsInput;
+ const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
+ let burgsAdded = 0;
+
+ const burgsTree = burgs[0];
+ let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between towns
+
+ while (burgsAdded < burgsNumber && spacing > 1) {
+ for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
+ if (cells.burg[sorted[i]]) continue;
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+ const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
+ if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
+ const burg = burgs.length;
+ const culture = cells.culture[cell];
+ const name = Names.getCulture(culture);
+ burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature: cells.f[cell]});
+ burgsTree.add([x, y]);
+ cells.burg[cell] = burg;
+ burgsAdded++;
+ }
+ spacing *= 0.5;
+ }
+
+ if (config.manorsInput != 1000 && burgsAdded < desiredNumber) {
+ ERROR && console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
+ }
+
+ burgs[0] = {name: undefined}; // do not store burgsTree anymore
+ TIME && console.timeEnd("placeTowns");
+}
+
+// define burg coordinates, coa, port status and define details
+export const specifyBurgs = (pack, grid, utils) => {
+ const {TIME, rn, gauss, P, COA} = utils;
+ TIME && console.time("specifyBurgs");
+ const {cells, features} = pack;
+ const temp = grid.cells.temp;
+
+ for (const b of pack.burgs) {
+ if (!b.i || b.lock) continue;
+ const i = b.cell;
+
+ // asign port status to some coastline burgs with temp > 0 °C
+ const haven = cells.haven[i];
+ if (haven && temp[cells.g[i]] > 0) {
+ const f = cells.f[haven]; // water body id
+ // port is a capital with any harbor OR town with good harbor
+ const port = features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
+ b.port = port ? f : 0; // port is defined by water body id it lays on
+ } else b.port = 0;
+
+ // define burg population (keep urbanization at about 10% rate)
+ b.population = rn(Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
+ if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
+
+ if (b.port) {
+ b.population = b.population * 1.3; // increase port population
+ const [x, y] = getCloseToEdgePoint(i, haven, pack, utils);
+ b.x = x;
+ b.y = y;
+ }
+
+ // add random factor
+ b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
+
+ // shift burgs on rivers semi-randomly and just a bit
+ if (!b.port && cells.r[i]) {
+ const shift = Math.min(cells.fl[i] / 150, 1);
+ if (i % 2) b.x = rn(b.x + shift, 2);
+ else b.x = rn(b.x - shift, 2);
+ if (cells.r[i] % 2) b.y = rn(b.y + shift, 2);
+ else b.y = rn(b.y - shift, 2);
+ }
+
+ // define emblem
+ const state = pack.states[b.state];
+ const stateCOA = state.coa;
+ let kinship = 0.25;
+ if (b.capital) kinship += 0.1;
+ else if (b.port) kinship -= 0.1;
+ if (b.culture !== state.culture) kinship -= 0.25;
+ b.type = getType(i, b.port, pack, utils);
+ const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
+ b.coa = COA.generate(stateCOA, kinship, null, type);
+ b.coa.shield = COA.getShield(b.culture, b.state);
+ }
+
+ // de-assign port status if it's the only one on feature
+ const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
+ for (const f of features) {
+ if (!f.i || f.land || f.border) continue;
+ const featurePorts = ports.filter(b => b.port === f.i);
+ if (featurePorts.length === 1) featurePorts[0].port = 0;
+ }
+
+ TIME && console.timeEnd("specifyBurgs");
+};
+
+export function getCloseToEdgePoint(cell1, cell2, pack, utils) {
+ const {cells, vertices} = pack;
+ const {rn} = utils;
+
+ 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];
+}
+
+export const getType = (cellId, port, pack, utils) => {
+ const {cells, features, burgs} = 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";
+};
+
+export const defineBurgFeatures = (burg, pack, utils) => {
+ const {P} = utils;
+ const {cells} = pack;
+
+ pack.burgs
+ .filter(b => (burg ? b.i == burg.i : b.i && !b.removed && !b.lock))
+ .forEach(b => {
+ const pop = b.population;
+ b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
+ b.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
+ b.walls = Number(b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
+ b.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)));
+ const religion = cells.religion[b.cell];
+ const theocracy = pack.states[b.state].form === "Theocracy";
+ b.temple = Number(
+ (religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
+ );
+ });
+};
+
+// expand cultures across the map (Dijkstra-like algorithm)
+export const expandStates = (pack, grid, config, utils) => {
+ const {TIME, FlatQueue, minmax, biomesData} = utils;
+ 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 = config.growthRate || 1;
+ const statesGrowthRate = config.statesGrowthRate || 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");
+};
+
+export const normalizeStates = (pack, utils) => {
+ const {TIME} = utils;
+ 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
+export const getPoles = (pack, utils) => {
+ const {getPolesOfInaccessibility} = utils;
+ const getType = cellId => pack.cells.state[cellId];
+ const poles = getPolesOfInaccessibility(pack, getType);
+
+ pack.states.forEach(s => {
+ if (!s.i || s.removed) return;
+ s.pole = poles[s.i] || [0, 0];
+ });
+};
+
+// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
+export const updateCultures = (pack, utils) => {
+ const {TIME} = utils;
+ TIME && console.time("updateCulturesForBurgsAndStates");
+
+ // Assign the culture associated with the burgs cell
+ pack.burgs = pack.burgs.map((burg, index) => {
+ if (index === 0) return burg;
+ return {...burg, culture: pack.cells.culture[burg.cell]};
+ });
+
+ // Assign the culture associated with the states' center cell
+ pack.states = pack.states.map((state, index) => {
+ if (index === 0) return state;
+ return {...state, culture: pack.cells.culture[state.center]};
+ });
+
+ TIME && console.timeEnd("updateCulturesForBurgsAndStates");
+};
+
+// calculate states data like area, population etc.
+export const collectStatistics = (pack) => {
+ const {cells, states} = pack;
+
+ states.forEach(s => {
+ if (s.removed) return;
+ s.cells = s.area = s.burgs = s.rural = s.urban = 0;
+ s.neighbors = new Set();
+ });
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue;
+ const s = cells.state[i];
+
+ // check for neighboring states
+ cells.c[i]
+ .filter(c => cells.h[c] >= 20 && cells.state[c] !== s)
+ .forEach(c => states[s].neighbors.add(cells.state[c]));
+
+ // 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++;
+ }
+ }
+
+ // convert neighbors Set object into array
+ states.forEach(s => {
+ if (!s.neighbors) return;
+ s.neighbors = Array.from(s.neighbors);
+ });
+};
+
+export const assignColors = (pack, utils) => {
+ const {TIME, getRandomColor, getMixedColor} = utils;
+ TIME && console.time("assignColors");
+ const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
+
+ // assign basic color using greedy coloring algorithm
+ pack.states.forEach(s => {
+ if (!s.i || s.removed || s.lock) return;
+ const neibs = s.neighbors;
+ s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
+ if (!s.color) s.color = getRandomColor();
+ colors.push(colors.shift());
+ });
+
+ // randomize each already used color a bit
+ colors.forEach(c => {
+ const sameColored = pack.states.filter(s => s.color === c && !s.lock);
+ sameColored.forEach((s, d) => {
+ if (!d) return;
+ s.color = getMixedColor(s.color);
+ });
+ });
+
+ TIME && console.timeEnd("assignColors");
+};
+
+const wars = {
+ War: 6,
+ Conflict: 2,
+ Campaign: 4,
+ Invasion: 2,
+ Rebellion: 2,
+ Conquest: 2,
+ Intervention: 1,
+ Expedition: 1,
+ Crusade: 1
+};
+
+export const generateCampaign = (state, pack, utils) => {
+ const {P, gauss, rw, getAdjective, Names, options} = utils;
+ 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
+export const generateCampaigns = (pack, utils) => {
+ pack.states.forEach(s => {
+ if (!s.i || s.removed) return;
+ s.campaigns = generateCampaign(s, pack, utils);
+ });
+};
+
+// generate Diplomatic Relationships
+export const generateDiplomacy = (pack, utils) => {
+ const {TIME, d3, P, ra, gauss, rw, trimVowels, options} = utils;
+ 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
+export const defineStateForms = (list, pack, utils) => {
+ const {TIME, d3, P, rw, rand, trimVowels, getAdjective} = utils;
+ 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, pack, utils);
+ s.fullName = getFullName(s, utils);
+ }
+
+ function selectForm(s, tier, pack, utils) {
+ const {P, rand, rw, trimVowels} = utils;
+ 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"
+];
+
+export const getFullName = (state, utils) => {
+ const {getAdjective} = utils;
+ 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}`;
+};
\ No newline at end of file
diff --git a/procedural/src/engine/support/burgs-and-states_config.md b/procedural/src/engine/support/burgs-and-states_config.md
new file mode 100644
index 00000000..ba1f4f7e
--- /dev/null
+++ b/procedural/src/engine/support/burgs-and-states_config.md
@@ -0,0 +1,52 @@
+# Burgs and States Module - Configuration Properties
+
+The refactored `burgs-and-states.js` module requires the following configuration properties to be passed via the `config` object:
+
+## Required Config Properties
+
+### State Generation Configuration
+- **`statesNumber`** - Number of states to generate
+ - **Original DOM call:** `byId("statesNumber").value`
+ - **Line reference:** Line 98 in original code
+ - **Usage:** Determines how many capital cities and states to create
+
+- **`sizeVariety`** - Variety factor for state sizes
+ - **Original DOM call:** `byId("sizeVariety").value`
+ - **Line reference:** Line 159 in original code
+ - **Usage:** Controls randomization of state expansionism values
+
+### Settlement Configuration
+- **`manorsInput`** - Number of towns/settlements to generate
+ - **Original DOM call:** `manorsInput.value` and `manorsInput.valueAsNumber`
+ - **Line references:** Lines 193, 195, 220 in original code
+ - **Usage:** Controls the number of secondary settlements (towns) to place
+
+### Growth Rate Configuration
+- **`growthRate`** - Global growth rate multiplier
+ - **Original DOM call:** `byId("growthRate").valueAsNumber`
+ - **Line reference:** Line 363 in original code
+ - **Usage:** Controls how aggressively states expand during the expansion phase
+
+- **`statesGrowthRate`** - State-specific growth rate multiplier
+ - **Original DOM call:** `byId("statesGrowthRate")?.valueAsNumber`
+ - **Line reference:** Line 364 in original code
+ - **Usage:** Additional multiplier specifically for state expansion behavior
+
+## Config Object Structure
+
+```javascript
+const config = {
+ statesNumber: 15, // Number of states to generate
+ sizeVariety: 1, // State size variety factor
+ manorsInput: 1000, // Number of towns (1000 = auto-calculate)
+ growthRate: 1, // Global growth rate multiplier
+ statesGrowthRate: 1 // State growth rate multiplier
+};
+```
+
+## Usage Notes
+
+- **`manorsInput`**: When set to `1000`, the system auto-calculates the number of towns based on available populated cells
+- **Growth rates**: Both growth rate properties default to `1` if not provided
+- **`statesGrowthRate`**: Uses optional chaining (`?.`) in original code, indicating it might not always be present
+- All numeric values are converted using `+` operator or `.valueAsNumber` in the original DOM calls
\ No newline at end of file
diff --git a/procedural/src/engine/support/burgs-and-states_external.md b/procedural/src/engine/support/burgs-and-states_external.md
new file mode 100644
index 00000000..a86f2a0f
--- /dev/null
+++ b/procedural/src/engine/support/burgs-and-states_external.md
@@ -0,0 +1,90 @@
+# Burgs and States Module - External Dependencies
+
+The refactored `burgs-and-states.js` module requires the following external dependencies to be injected via the `utils` parameter:
+
+## Required Utilities
+
+### Core Utilities
+- **`TIME`** - Global timing flag for performance monitoring (boolean)
+- **`WARN`** - Warning logging flag (boolean)
+- **`ERROR`** - Error logging flag (boolean)
+- **`d3`** - D3.js library for mathematical functions and data structures
+ - `d3.quadtree()` - Spatial data structure for efficient proximity searches
+ - `d3.mean()` - Calculate mean values
+ - `d3.median()` - Calculate median values
+ - `d3.sum()` - Calculate sum of arrays
+- **`rn`** - Rounding utility function for numerical precision
+- **`P`** - Probability utility function for random boolean generation
+- **`gauss`** - Gaussian/normal distribution random number generator
+- **`ra`** - Random array element selector
+- **`rw`** - Weighted random selector
+- **`minmax`** - Min/max clamping utility
+- **`each`** - Utility for creating interval checkers
+- **`rand`** - Random number generator
+
+### External Modules
+- **`Names`** - Name generation module
+ - `Names.getCultureShort()` - Generate short cultural names
+ - `Names.getState()` - Generate state names
+ - `Names.getCulture()` - Generate cultural names
+- **`COA`** - Coat of Arms generation module
+ - `COA.generate()` - Generate coat of arms
+ - `COA.getShield()` - Generate shield designs
+- **`biomesData`** - Biome data containing cost arrays
+- **`options`** - Global options object containing year settings
+- **`FlatQueue`** - Priority queue implementation for pathfinding
+
+### Color Utilities
+- **`getColors`** - Generate color palettes
+- **`getRandomColor`** - Generate random colors
+- **`getMixedColor`** - Create color variations
+
+### String Utilities
+- **`getAdjective`** - Convert nouns to adjectives
+- **`trimVowels`** - Remove vowels from strings
+
+### Geometric Utilities
+- **`getPolesOfInaccessibility`** - Calculate pole of inaccessibility for polygons
+
+### Graph Properties
+- **`graphWidth`** - Width of the generated graph
+- **`graphHeight`** - Height of the generated graph
+
+## Import Structure
+
+When integrating this module, the calling code should provide these utilities:
+
+```javascript
+import { generate, expandStates, specifyBurgs, /* other functions */ } from './burgs-and-states.js';
+
+const utils = {
+ TIME: globalTimeFlag,
+ WARN: warnFlag,
+ ERROR: errorFlag,
+ d3: d3Library,
+ rn: roundingFunction,
+ P: probabilityFunction,
+ gauss: gaussianRandom,
+ ra: randomArrayElement,
+ rw: weightedRandom,
+ minmax: minMaxClamp,
+ each: intervalChecker,
+ rand: randomGenerator,
+ Names: namesModule,
+ COA: coaModule,
+ biomesData: biomesDataObject,
+ options: globalOptions,
+ FlatQueue: flatQueueClass,
+ getColors: colorGenerator,
+ getRandomColor: randomColorGenerator,
+ getMixedColor: colorMixer,
+ getAdjective: adjectiveConverter,
+ trimVowels: vowelTrimmer,
+ getPolesOfInaccessibility: poleCalculator,
+ graphWidth: mapWidth,
+ graphHeight: mapHeight
+};
+
+// Usage
+const result = generate(pack, grid, config, utils);
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/burgs-and-states_prompt.md b/procedural/src/engine/support/burgs-and-states_prompt.md
new file mode 100644
index 00000000..95ad3d2f
--- /dev/null
+++ b/procedural/src/engine/support/burgs-and-states_prompt.md
@@ -0,0 +1,967 @@
+# burgs-and-states.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.txt`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `burgs-and-states.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.BurgsAndStates = (() => {
+ const generate = () => {
+ const {cells, cultures} = pack;
+ const n = cells.i.length;
+
+ cells.burg = new Uint16Array(n); // cell burg
+
+ const burgs = (pack.burgs = placeCapitals());
+ pack.states = createStates();
+
+ placeTowns();
+ expandStates();
+ normalizeStates();
+ getPoles();
+
+ specifyBurgs();
+
+ collectStatistics();
+ assignColors();
+
+ generateCampaigns();
+ generateDiplomacy();
+
+ function placeCapitals() {
+ TIME && console.time("placeCapitals");
+ let count = +byId("statesNumber").value;
+ let burgs = [0];
+
+ const rand = () => 0.5 + Math.random() * 0.5;
+ const score = new Int16Array(cells.s.map(s => s * rand())); // cell score for capitals placement
+ const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ if (sorted.length < count * 10) {
+ count = Math.floor(sorted.length / 10);
+ if (!count) {
+ WARN && console.warn("There is no populated cells. Cannot generate states");
+ return burgs;
+ } else {
+ WARN && console.warn(`Not enough populated cells (${sorted.length}). Will generate only ${count} states`);
+ }
+ }
+
+ let burgsTree = d3.quadtree();
+ let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
+
+ for (let i = 0; burgs.length <= count; i++) {
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+
+ if (burgsTree.find(x, y, spacing) === undefined) {
+ burgs.push({cell, x, y});
+ burgsTree.add([x, y]);
+ }
+
+ if (i === sorted.length - 1) {
+ WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
+ burgsTree = d3.quadtree();
+ i = -1;
+ burgs = [0];
+ spacing /= 1.2;
+ }
+ }
+
+ burgs[0] = burgsTree;
+ TIME && console.timeEnd("placeCapitals");
+ return burgs;
+ }
+
+ // For each capital create a state
+ function createStates() {
+ TIME && console.time("createStates");
+ const states = [{i: 0, name: "Neutrals"}];
+ const colors = getColors(burgs.length - 1);
+ const each5th = each(5);
+
+ burgs.forEach((b, i) => {
+ if (!i) return; // skip first element
+
+ // burgs data
+ b.i = b.state = i;
+ b.culture = cells.culture[b.cell];
+ b.name = Names.getCultureShort(b.culture);
+ b.feature = cells.f[b.cell];
+ b.capital = 1;
+
+ // states data
+ const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1);
+ const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
+ const name = Names.getState(basename, b.culture);
+ const type = cultures[b.culture].type;
+
+ const coa = COA.generate(null, null, null, type);
+ coa.shield = COA.getShield(b.culture, null);
+ states.push({
+ i,
+ color: colors[i - 1],
+ name,
+ expansionism,
+ capital: i,
+ type,
+ center: b.cell,
+ culture: b.culture,
+ coa
+ });
+ cells.burg[b.cell] = i;
+ });
+
+ TIME && console.timeEnd("createStates");
+ return states;
+ }
+
+ // place secondary settlements based on geo and economical evaluation
+ function placeTowns() {
+ TIME && console.time("placeTowns");
+ const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
+ const sorted = cells.i
+ .filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i])
+ .sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+
+ const desiredNumber =
+ manorsInput.value == 1000
+ ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8)
+ : manorsInput.valueAsNumber;
+ const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
+ let burgsAdded = 0;
+
+ const burgsTree = burgs[0];
+ let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between towns
+
+ while (burgsAdded < burgsNumber && spacing > 1) {
+ for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
+ if (cells.burg[sorted[i]]) continue;
+ const cell = sorted[i];
+ const [x, y] = cells.p[cell];
+ const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
+ if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
+ const burg = burgs.length;
+ const culture = cells.culture[cell];
+ const name = Names.getCulture(culture);
+ burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature: cells.f[cell]});
+ burgsTree.add([x, y]);
+ cells.burg[cell] = burg;
+ burgsAdded++;
+ }
+ spacing *= 0.5;
+ }
+
+ if (manorsInput.value != 1000 && burgsAdded < desiredNumber) {
+ ERROR && console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
+ }
+
+ burgs[0] = {name: undefined}; // do not store burgsTree anymore
+ TIME && console.timeEnd("placeTowns");
+ }
+ };
+
+ // define burg coordinates, coa, port status and define details
+ const specifyBurgs = () => {
+ TIME && console.time("specifyBurgs");
+ const {cells, features} = pack;
+ const temp = grid.cells.temp;
+
+ for (const b of pack.burgs) {
+ if (!b.i || b.lock) continue;
+ const i = b.cell;
+
+ // asign port status to some coastline burgs with temp > 0 °C
+ const haven = cells.haven[i];
+ if (haven && temp[cells.g[i]] > 0) {
+ const f = cells.f[haven]; // water body id
+ // port is a capital with any harbor OR town with good harbor
+ const port = features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
+ b.port = port ? f : 0; // port is defined by water body id it lays on
+ } else b.port = 0;
+
+ // define burg population (keep urbanization at about 10% rate)
+ b.population = rn(Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
+ if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
+
+ if (b.port) {
+ b.population = b.population * 1.3; // increase port population
+ const [x, y] = getCloseToEdgePoint(i, haven);
+ b.x = x;
+ b.y = y;
+ }
+
+ // add random factor
+ b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
+
+ // shift burgs on rivers semi-randomly and just a bit
+ if (!b.port && cells.r[i]) {
+ const shift = Math.min(cells.fl[i] / 150, 1);
+ if (i % 2) b.x = rn(b.x + shift, 2);
+ else b.x = rn(b.x - shift, 2);
+ if (cells.r[i] % 2) b.y = rn(b.y + shift, 2);
+ else b.y = rn(b.y - shift, 2);
+ }
+
+ // define emblem
+ const state = pack.states[b.state];
+ const stateCOA = state.coa;
+ let kinship = 0.25;
+ if (b.capital) kinship += 0.1;
+ else if (b.port) kinship -= 0.1;
+ if (b.culture !== state.culture) kinship -= 0.25;
+ b.type = getType(i, b.port);
+ const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
+ b.coa = COA.generate(stateCOA, kinship, null, type);
+ b.coa.shield = COA.getShield(b.culture, b.state);
+ }
+
+ // de-assign port status if it's the only one on feature
+ const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
+ for (const f of features) {
+ if (!f.i || f.land || f.border) continue;
+ const featurePorts = ports.filter(b => b.port === f.i);
+ if (featurePorts.length === 1) featurePorts[0].port = 0;
+ }
+
+ TIME && console.timeEnd("specifyBurgs");
+ };
+
+ 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 getType = (cellId, port) => {
+ const {cells, features, burgs} = 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";
+ };
+
+ const defineBurgFeatures = burg => {
+ const {cells} = pack;
+
+ pack.burgs
+ .filter(b => (burg ? b.i == burg.i : b.i && !b.removed && !b.lock))
+ .forEach(b => {
+ const pop = b.population;
+ b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
+ b.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
+ b.walls = Number(b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
+ b.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)));
+ const religion = cells.religion[b.cell];
+ const theocracy = pack.states[b.state].form === "Theocracy";
+ b.temple = Number(
+ (religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
+ );
+ });
+ };
+
+ // 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 normalizeStates = () => {
+ 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];
+ });
+ };
+
+ // Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
+ const updateCultures = () => {
+ TIME && console.time("updateCulturesForBurgsAndStates");
+
+ // Assign the culture associated with the burgs cell
+ pack.burgs = pack.burgs.map((burg, index) => {
+ if (index === 0) return burg;
+ return {...burg, culture: pack.cells.culture[burg.cell]};
+ });
+
+ // Assign the culture associated with the states' center cell
+ pack.states = pack.states.map((state, index) => {
+ if (index === 0) return state;
+ return {...state, culture: pack.cells.culture[state.center]};
+ });
+
+ TIME && console.timeEnd("updateCulturesForBurgsAndStates");
+ };
+
+ // 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;
+ s.neighbors = new Set();
+ });
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue;
+ const s = cells.state[i];
+
+ // check for neighboring states
+ cells.c[i]
+ .filter(c => cells.h[c] >= 20 && cells.state[c] !== s)
+ .forEach(c => states[s].neighbors.add(cells.state[c]));
+
+ // 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++;
+ }
+ }
+
+ // convert neighbors Set object into array
+ states.forEach(s => {
+ if (!s.neighbors) return;
+ s.neighbors = Array.from(s.neighbors);
+ });
+
+ TIME && console.timeEnd("collectStatistics");
+ };
+
+ const assignColors = () => {
+ TIME && console.time("assignColors");
+ const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
+
+ // assign basic color using greedy coloring algorithm
+ pack.states.forEach(s => {
+ if (!s.i || s.removed || s.lock) return;
+ const neibs = s.neighbors;
+ s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
+ if (!s.color) s.color = getRandomColor();
+ colors.push(colors.shift());
+ });
+
+ // randomize each already used color a bit
+ colors.forEach(c => {
+ const sameColored = pack.states.filter(s => s.color === c && !s.lock);
+ sameColored.forEach((s, d) => {
+ if (!d) return;
+ s.color = getMixedColor(s.color);
+ });
+ });
+
+ TIME && console.timeEnd("assignColors");
+ };
+
+ 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,
+ normalizeStates,
+ getPoles,
+ assignColors,
+ specifyBurgs,
+ defineBurgFeatures,
+ getType,
+ collectStatistics,
+ generateCampaign,
+ generateCampaigns,
+ generateDiplomacy,
+ defineStateForms,
+ getFullName,
+ updateCultures,
+ getCloseToEdgePoint
+ };
+})();
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./burgs-and-states.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./burgs-and-states_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in burgs-and-states_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into burgs-and-states_render.md
diff --git a/procedural/src/engine/support/burgs-and-states_render.md b/procedural/src/engine/support/burgs-and-states_render.md
new file mode 100644
index 00000000..647b757e
--- /dev/null
+++ b/procedural/src/engine/support/burgs-and-states_render.md
@@ -0,0 +1,35 @@
+# Burgs and States Module - Removed Rendering/UI Logic
+
+After analyzing the original `burgs-and-states.js` code, **no rendering or UI logic was found to remove**.
+
+## Analysis Results
+
+The module contains only:
+
+- **Data structure generation** (burgs, states arrays)
+- **Pure computational logic** for placement algorithms
+- **Mathematical calculations** for state expansion and diplomacy
+- **Statistical calculations** for population and area
+- **Algorithmic processing** for territorial assignment
+
+## No Rendering Logic Found
+
+The original module was already focused purely on data generation without any:
+
+- ❌ DOM manipulation (no `d3.select`, `document.getElementById`, etc.)
+- ❌ SVG rendering (no path creation, element styling, etc.)
+- ❌ Canvas drawing operations
+- ❌ HTML element creation or modification
+- ❌ CSS style manipulation
+- ❌ UI event handling
+
+## Module Characteristics
+
+This module represents a **pure computational engine** that:
+
+1. **Receives data** (`pack`, `grid`) as input
+2. **Applies algorithms** for territorial and settlement generation
+3. **Returns structured data** for use by rendering systems
+4. **Contains no visual output** or DOM dependencies
+
+The separation of concerns was already well-maintained in the original codebase for this particular module, requiring only the removal of global state dependencies and DOM-based configuration reading.
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-generator_config.md b/procedural/src/engine/support/coa-generator_config.md
new file mode 100644
index 00000000..e18901d0
--- /dev/null
+++ b/procedural/src/engine/support/coa-generator_config.md
@@ -0,0 +1,40 @@
+# Configuration Properties for coa-generator.js
+
+The refactored `coa-generator.js` module requires a `config` object with the following properties:
+
+## Required Config Properties
+
+### emblemShape
+- **Type**: String
+- **Description**: The selected emblem shape value from the UI dropdown
+- **Original DOM Call**: `document.getElementById("emblemShape").value`
+- **Usage**: Used in `getShield()` function to determine shield type
+
+### emblemShapeGroup
+- **Type**: String
+- **Description**: The parent group label of the selected emblem shape option
+- **Original DOM Call**: `emblemShape.selectedOptions[0]?.parentNode.label`
+- **Default**: "Diversiform" when no parent group exists
+- **Usage**: Used in `getShield()` function to determine if custom shield logic should be applied
+
+## Config Object Structure
+
+The config object should be structured as follows:
+
+```javascript
+const config = {
+ emblemShape: "heater", // Value from emblem shape selector
+ emblemShapeGroup: "Basic" // Parent group of the selected option
+};
+```
+
+## Function Signatures
+
+### getShield(pack, culture, state, config)
+The `getShield` function now accepts the config object as the fourth parameter instead of reading from the DOM directly.
+
+## Migration Notes
+
+- The original code read these values directly from the DOM using `byId("emblemShape")`
+- The refactored version receives these values through the config parameter
+- The calling code (Viewer/Client) is responsible for reading from the DOM and passing these values to the engine
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-generator_external.md b/procedural/src/engine/support/coa-generator_external.md
new file mode 100644
index 00000000..7f2ad0f5
--- /dev/null
+++ b/procedural/src/engine/support/coa-generator_external.md
@@ -0,0 +1,32 @@
+# External Dependencies for coa-generator.js
+
+The refactored `coa-generator.js` module requires the following external dependencies to be imported:
+
+## Required Utility Functions
+
+The module expects a `utils` object containing:
+
+- **`P(probability)`** - Probability function that returns true/false based on given probability (0-1)
+- **`rw(weightedObject)`** - Random weighted selection function that picks a key from an object based on weighted values
+
+These utilities should be imported from a shared utilities module in the engine.
+
+## Required Data Objects
+
+The following data objects must be passed as parameters:
+
+- **`pack`** - The main game data object containing:
+ - `pack.states` - Array of state objects with COA data
+ - `pack.cultures` - Array of culture objects with shield preferences
+
+## External Error Handling
+
+The original code referenced a global `ERROR` variable for error logging. The refactored version removes this dependency. Error handling should now be implemented by the calling code or through a logging utility passed in the utils object if needed.
+
+## Removed Global Dependencies
+
+The following global dependencies have been removed:
+- `window` object attachment
+- `document` object access
+- `byId()` DOM utility function
+- Global `ERROR` variable
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-generator_prompt.md b/procedural/src/engine/support/coa-generator_prompt.md
new file mode 100644
index 00000000..74d120e4
--- /dev/null
+++ b/procedural/src/engine/support/coa-generator_prompt.md
@@ -0,0 +1,2295 @@
+# coa-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `coa-generator.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.COA = (function () {
+ const tinctures = {
+ field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1},
+ division: {metals: 5, colours: 8, stains: +P(0.03), patterns: 1},
+ charge: {metals: 2, colours: 3, stains: +P(0.05), patterns: 0},
+ metals: {argent: 3, or: 2},
+ colours: {gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2},
+ stains: {murrey: 1, sanguine: 1, tenné: 1},
+ patterns: {
+ semy: 8,
+ ermine: 6,
+ vair: 4,
+ counterVair: 1,
+ vairInPale: 1,
+ vairEnPointe: 2,
+ vairAncien: 2,
+ potent: 2,
+ counterPotent: 1,
+ potentInPale: 1,
+ potentEnPointe: 1,
+ chequy: 8,
+ lozengy: 5,
+ fusily: 2,
+ pally: 8,
+ barry: 10,
+ gemelles: 1,
+ bendy: 8,
+ bendySinister: 4,
+ palyBendy: 2,
+ barryBendy: 1,
+ pappellony: 2,
+ pappellony2: 3,
+ scaly: 1,
+ plumetty: 1,
+ masoned: 6,
+ fretty: 3,
+ grillage: 1,
+ chainy: 1,
+ maily: 2,
+ honeycombed: 1
+ }
+ };
+
+ const chargeData = {
+ agnusDei: {
+ colors: 2,
+ sinister: true
+ },
+ angel: {
+ colors: 2,
+ positions: {e: 1}
+ },
+ anvil: {
+ sinister: true
+ },
+ apple: {
+ colors: 2
+ },
+ arbalest: {
+ colors: 3,
+ reversed: true
+ },
+ archer: {
+ colors: 3,
+ sinister: true
+ },
+ armEmbowedHoldingSabre: {
+ colors: 3,
+ sinister: true
+ },
+ armEmbowedVambraced: {
+ sinister: true
+ },
+ armEmbowedVambracedHoldingSword: {
+ colors: 3,
+ sinister: true
+ },
+ armillarySphere: {
+ positions: {e: 1}
+ },
+ arrow: {
+ colors: 3,
+ reversed: true
+ },
+ arrowsSheaf: {
+ colors: 3,
+ reversed: true
+ },
+ axe: {
+ colors: 2,
+ sinister: true
+ },
+ badgerStatant: {
+ colors: 2,
+ sinister: true
+ },
+ banner: {
+ colors: 2
+ },
+ basilisk: {
+ colors: 3,
+ sinister: true
+ },
+ bearPassant: {
+ colors: 3,
+ sinister: true
+ },
+ bearRampant: {
+ colors: 3,
+ sinister: true
+ },
+ bee: {
+ colors: 3,
+ reversed: true
+ },
+ bell: {
+ colors: 2
+ },
+ boarHeadErased: {
+ colors: 3,
+ sinister: true
+ },
+ boarRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 12, beh: 1, kn: 1, jln: 2}
+ },
+ boat: {
+ colors: 2
+ },
+ bookClosed: {
+ colors: 3,
+ sinister: true
+ },
+ bookClosed2: {
+ sinister: true
+ },
+ bookOpen: {
+ colors: 3
+ },
+ bow: {
+ sinister: true
+ },
+ bowWithArrow: {
+ colors: 3,
+ reversed: true
+ },
+ bowWithThreeArrows: {
+ colors: 3
+ },
+ bucket: {
+ colors: 2
+ },
+ bugleHorn: {
+ colors: 2
+ },
+ bugleHorn2: {
+ colors: 2
+ },
+ bullHeadCaboshed: {
+ colors: 2
+ },
+ bullPassant: {
+ colors: 3,
+ sinister: true
+ },
+ butterfly: {
+ colors: 3,
+ reversed: true
+ },
+ camel: {
+ colors: 2,
+ sinister: true
+ },
+ cancer: {
+ reversed: true
+ },
+ cannon: {
+ colors: 2,
+ sinister: true
+ },
+ caravel: {
+ colors: 3,
+ sinister: true
+ },
+ castle: {
+ colors: 2
+ },
+ castle2: {
+ colors: 3
+ },
+ catPassantGuardant: {
+ colors: 2,
+ sinister: true
+ },
+ cavalier: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ centaur: {
+ colors: 3,
+ sinister: true
+ },
+ chalice: {
+ colors: 2
+ },
+ cinquefoil: {
+ reversed: true
+ },
+ cock: {
+ colors: 3,
+ sinister: true
+ },
+ comet: {
+ reversed: true
+ },
+ cowStatant: {
+ colors: 3,
+ sinister: true
+ },
+ cossack: {
+ colors: 3,
+ sinister: true
+ },
+ crescent: {
+ reversed: true
+ },
+ crocodile: {
+ colors: 2,
+ sinister: true
+ },
+ crosier: {
+ sinister: true
+ },
+ crossbow: {
+ colors: 3,
+ sinister: true
+ },
+ crossGamma: {
+ sinister: true
+ },
+ crossLatin: {
+ reversed: true
+ },
+ crossTau: {
+ reversed: true
+ },
+ crossTriquetra: {
+ reversed: true
+ },
+ crown: {
+ colors: 2,
+ positions: {
+ e: 10,
+ abcdefgzi: 1,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 1,
+ pq: 2,
+ abc: 1,
+ jln: 4,
+ jleh: 1,
+ def: 2,
+ abcpqh: 3
+ }
+ },
+ crown2: {
+ colors: 3,
+ positions: {
+ e: 10,
+ abcdefgzi: 1,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 1,
+ pq: 2,
+ abc: 1,
+ jln: 4,
+ jleh: 1,
+ def: 2,
+ abcpqh: 3
+ }
+ },
+ deerHeadCaboshed: {
+ colors: 2
+ },
+ dolphin: {
+ colors: 2,
+ sinister: true
+ },
+ donkeyHeadCaboshed: {
+ colors: 2
+ },
+ dove: {
+ colors: 2,
+ natural: "argent",
+ sinister: true
+ },
+ doveDisplayed: {
+ colors: 2,
+ natural: "argent",
+ sinister: true
+ },
+ dragonfly: {
+ colors: 2,
+ reversed: true
+ },
+ dragonPassant: {
+ colors: 3,
+ sinister: true
+ },
+ dragonRampant: {
+ colors: 3,
+ sinister: true
+ },
+ drakkar: {
+ colors: 3,
+ sinister: true
+ },
+ drawingCompass: {
+ sinister: true
+ },
+ drum: {
+ colors: 3
+ },
+ duck: {
+ colors: 3,
+ sinister: true
+ },
+ eagle: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1}
+ },
+ eagleTwoHeads: {
+ colors: 3
+ },
+ elephant: {
+ colors: 2,
+ sinister: true
+ },
+ elephantHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ falchion: {
+ colors: 2,
+ reversed: true
+ },
+ falcon: {
+ colors: 3,
+ sinister: true
+ },
+ fan: {
+ colors: 2,
+ reversed: true
+ },
+ fasces: {
+ colors: 3,
+ sinister: true
+ },
+ feather: {
+ sinister: true
+ },
+ flamberge: {
+ colors: 2,
+ reversed: true
+ },
+ flangedMace: {
+ reversed: true
+ },
+ fly: {
+ colors: 3,
+ reversed: true
+ },
+ foot: {
+ sinister: true
+ },
+ fountain: {
+ natural: "azure"
+ },
+ frog: {
+ reversed: true
+ },
+ garb: {
+ colors: 2,
+ natural: "or",
+ positions: {e: 1, def: 3, abc: 2, beh: 1, kn: 1, jln: 3, jleh: 1, abcpqh: 1, joe: 1, lme: 1}
+ },
+ gauntlet: {
+ sinister: true,
+ reversed: true
+ },
+ goat: {
+ colors: 3,
+ sinister: true
+ },
+ goutte: {
+ reversed: true
+ },
+ grapeBunch: {
+ colors: 3,
+ sinister: true
+ },
+ grapeBunch2: {
+ colors: 3,
+ sinister: true
+ },
+ grenade: {
+ colors: 2
+ },
+ greyhoundCourant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ greyhoundRampant: {
+ colors: 2,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ greyhoundSejant: {
+ colors: 3,
+ sinister: true
+ },
+ griffinPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1}
+ },
+ griffinRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ hand: {
+ sinister: true,
+ reversed: true,
+ positions: {e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1}
+ },
+ harp: {
+ colors: 2,
+ sinister: true
+ },
+ hatchet: {
+ colors: 2,
+ sinister: true
+ },
+ head: {
+ colors: 2,
+ sinister: true,
+ positions: {e: 1}
+ },
+ headWreathed: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ hedgehog: {
+ colors: 3,
+ sinister: true
+ },
+ helmet: {
+ sinister: true
+ },
+ helmetCorinthian: {
+ colors: 3,
+ sinister: true
+ },
+ helmetGreat: {
+ sinister: true
+ },
+ helmetZischagge: {
+ sinister: true
+ },
+ heron: {
+ colors: 2,
+ sinister: true
+ },
+ hindStatant: {
+ colors: 2,
+ sinister: true
+ },
+ hook: {
+ sinister: true
+ },
+ horseHeadCouped: {
+ sinister: true
+ },
+ horsePassant: {
+ colors: 2,
+ sinister: true
+ },
+ horseRampant: {
+ colors: 3,
+ sinister: true
+ },
+ horseSalient: {
+ colors: 2,
+ sinister: true
+ },
+ horseshoe: {
+ reversed: true
+ },
+ hourglass: {
+ colors: 3
+ },
+ ladybird: {
+ colors: 3,
+ reversed: true
+ },
+ lamb: {
+ colors: 2,
+ sinister: true
+ },
+ lambPassantReguardant: {
+ colors: 2,
+ sinister: true
+ },
+ lanceWithBanner: {
+ colors: 3,
+ sinister: true
+ },
+ laurelWreath: {
+ colors: 2
+ },
+ lighthouse: {
+ colors: 3
+ },
+ lionHeadCaboshed: {
+ colors: 2
+ },
+ lionHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ lionPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ lionPassantGuardant: {
+ colors: 3,
+ sinister: true
+ },
+ lionRampant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1}
+ },
+ lionSejant: {
+ colors: 3,
+ sinister: true
+ },
+ lizard: {
+ reversed: true
+ },
+ lochaberAxe: {
+ colors: 2,
+ sinister: true
+ },
+ log: {
+ sinister: true
+ },
+ lute: {
+ colors: 2,
+ sinister: true
+ },
+ lymphad: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 1}
+ },
+ mace: {
+ colors: 2
+ },
+ maces: {
+ colors: 2
+ },
+ mallet: {
+ colors: 2
+ },
+ mantle: {
+ colors: 3
+ },
+ martenCourant: {
+ colors: 3,
+ sinister: true
+ },
+ mascle: {
+ positions: {
+ e: 15,
+ abcdefgzi: 3,
+ beh: 3,
+ bdefh: 4,
+ acegi: 1,
+ kn: 3,
+ joe: 2,
+ abc: 3,
+ jlh: 8,
+ jleh: 1,
+ df: 3,
+ abcpqh: 4,
+ pqe: 3,
+ eknpq: 3
+ }
+ },
+ mastiffStatant: {
+ colors: 3,
+ sinister: true
+ },
+ mitre: {
+ colors: 3
+ },
+ monk: {
+ sinister: true
+ },
+ moonInCrescent: {
+ sinister: true
+ },
+ mullet: {
+ reversed: true
+ },
+ mullet7: {
+ reversed: true
+ },
+ oak: {
+ colors: 3
+ },
+ orb: {
+ colors: 3
+ },
+ ouroboros: {
+ sinister: true
+ },
+ owl: {
+ colors: 2,
+ sinister: true
+ },
+ owlDisplayed: {
+ colors: 2
+ },
+ palmTree: {
+ colors: 3
+ },
+ parrot: {
+ colors: 2,
+ sinister: true
+ },
+ peacock: {
+ colors: 3,
+ sinister: true
+ },
+ peacockInPride: {
+ colors: 3,
+ sinister: true
+ },
+ pear: {
+ colors: 2
+ },
+ pegasus: {
+ colors: 3,
+ sinister: true
+ },
+ pike: {
+ colors: 2,
+ sinister: true
+ },
+ pineTree: {
+ colors: 2
+ },
+ plaice: {
+ colors: 2,
+ sinister: true
+ },
+ plough: {
+ colors: 2,
+ sinister: true
+ },
+ ploughshare: {
+ sinister: true
+ },
+ porcupine: {
+ colors: 2,
+ sinister: true
+ },
+ portcullis: {
+ colors: 2
+ },
+ rabbitSejant: {
+ colors: 2,
+ sinister: true
+ },
+ rake: {
+ reversed: true
+ },
+ rapier: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ ramHeadErased: {
+ colors: 3,
+ sinister: true
+ },
+ ramPassant: {
+ colors: 3,
+ sinister: true
+ },
+ ratRampant: {
+ colors: 2,
+ sinister: true
+ },
+ raven: {
+ colors: 2,
+ natural: "sable",
+ sinister: true,
+ positions: {e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1}
+ },
+ rhinoceros: {
+ colors: 2,
+ sinister: true
+ },
+ rose: {
+ colors: 3
+ },
+ sabre: {
+ colors: 2,
+ sinister: true
+ },
+ sabre2: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ sabresCrossed: {
+ colors: 2,
+ reversed: true
+ },
+ sagittarius: {
+ colors: 3,
+ sinister: true
+ },
+ salmon: {
+ colors: 2,
+ sinister: true
+ },
+ saw: {
+ colors: 2
+ },
+ scale: {
+ colors: 2
+ },
+ scaleImbalanced: {
+ colors: 2,
+ sinister: true
+ },
+ scissors: {
+ reversed: true
+ },
+ scorpion: {
+ reversed: true
+ },
+ scrollClosed: {
+ colors: 2,
+ sinister: true
+ },
+ scythe: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ scythe2: {
+ sinister: true
+ },
+ serpent: {
+ colors: 2,
+ sinister: true
+ },
+ shield: {
+ colors: 2,
+ sinister: true
+ },
+ sickle: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ snail: {
+ colors: 2,
+ sinister: true
+ },
+ snake: {
+ colors: 2,
+ sinister: true
+ },
+ spear: {
+ colors: 2,
+ reversed: true
+ },
+ spiral: {
+ sinister: true,
+ reversed: true
+ },
+ squirrel: {
+ sinister: true
+ },
+ stagLodgedRegardant: {
+ colors: 3,
+ sinister: true
+ },
+ stagPassant: {
+ colors: 2,
+ sinister: true
+ },
+ stirrup: {
+ colors: 2
+ },
+ swallow: {
+ colors: 2,
+ sinister: true
+ },
+ swan: {
+ colors: 3,
+ sinister: true
+ },
+ swanErased: {
+ colors: 3,
+ sinister: true
+ },
+ sword: {
+ colors: 2,
+ reversed: true
+ },
+ talbotPassant: {
+ colors: 3,
+ sinister: true
+ },
+ talbotSejant: {
+ colors: 3,
+ sinister: true
+ },
+ tower: {
+ colors: 2
+ },
+ tree: {
+ positions: {e: 1}
+ },
+ trefoil: {
+ reversed: true
+ },
+ trowel: {
+ colors: 2,
+ sinister: true,
+ reversed: true
+ },
+ unicornRampant: {
+ colors: 3,
+ sinister: true
+ },
+ wasp: {
+ colors: 3,
+ reversed: true
+ },
+ wheatStalk: {
+ colors: 2
+ },
+ windmill: {
+ colors: 3,
+ sinister: true
+ },
+ wing: {
+ sinister: true
+ },
+ wingSword: {
+ colors: 3,
+ sinister: true
+ },
+ wolfHeadErased: {
+ colors: 2,
+ sinister: true
+ },
+ wolfPassant: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1}
+ },
+ wolfRampant: {
+ colors: 3,
+ sinister: true
+ },
+ wolfStatant: {
+ colors: 3,
+ sinister: true
+ },
+ wyvern: {
+ colors: 3,
+ sinister: true,
+ positions: {e: 10, jln: 1}
+ },
+ wyvernWithWingsDisplayed: {
+ colors: 3,
+ sinister: true
+ }
+ };
+
+ const charges = {
+ types: {
+ conventional: 33, // 40 charges
+ crosses: 13, // 30 charges
+ beasts: 7, // 41 charges
+ beastHeads: 3, // 10 charges
+ birds: 3, // 16 charges
+ reptiles: 2, // 5 charges
+ bugs: 2, // 8 charges
+ fishes: 1, // 3 charges
+ molluscs: 1, // 2 charges
+ plants: 3, // 18 charges
+ fantastic: 5, // 14 charges
+ agriculture: 2, // 8 charges
+ arms: 5, // 32 charges
+ bodyparts: 2, // 12 charges
+ people: 2, // 4 charges
+ architecture: 3, // 11 charges
+ seafaring: 3, // 9 charges
+ tools: 3, // 15 charges
+ miscellaneous: 5, // 30 charges
+ inescutcheon: 3, // 43 charges
+ ornaments: 0, // 9 charges
+ uploaded: 0
+ },
+ single: {
+ conventional: 10,
+ crosses: 8,
+ beasts: 7,
+ beastHeads: 3,
+ birds: 3,
+ reptiles: 2,
+ bugs: 2,
+ fishes: 1,
+ molluscs: 1,
+ plants: 3,
+ fantastic: 5,
+ agriculture: 2,
+ arms: 5,
+ bodyparts: 2,
+ people: 2,
+ architecture: 3,
+ seafaring: 3,
+ tools: 3,
+ miscellaneous: 5,
+ inescutcheon: 1
+ },
+ semy: {
+ conventional: 4,
+ crosses: 1
+ },
+ conventional: {
+ annulet: 4,
+ billet: 5,
+ carreau: 1,
+ comet: 1,
+ compassRose: 1,
+ crescent: 5,
+ delf: 0,
+ estoile: 1,
+ fleurDeLis: 6,
+ fountain: 1,
+ fusil: 4,
+ gear: 1,
+ goutte: 4,
+ heart: 4,
+ lozenge: 2,
+ lozengeFaceted: 3,
+ lozengePloye: 1,
+ mascle: 4,
+ moonInCrescent: 1,
+ mullet: 5,
+ mullet10: 1,
+ mullet4: 3,
+ mullet6: 4,
+ mullet6Faceted: 1,
+ mullet6Pierced: 1,
+ mullet7: 1,
+ mullet8: 1,
+ mulletFaceted: 1,
+ mulletPierced: 1,
+ pique: 2,
+ roundel: 4,
+ roundel2: 3,
+ rustre: 2,
+ spiral: 1,
+ sun: 3,
+ sunInSplendour: 1,
+ sunInSplendour2: 1,
+ trefle: 2,
+ triangle: 3,
+ trianglePierced: 1
+ },
+ crosses: {
+ crossHummetty: 15,
+ crossVoided: 1,
+ crossPattee: 2,
+ crossPatteeAlisee: 1,
+ crossFormee: 1,
+ crossFormee2: 2,
+ crossPotent: 2,
+ crossJerusalem: 1,
+ crosslet: 1,
+ crossClechy: 3,
+ crossBottony: 1,
+ crossFleury: 3,
+ crossPatonce: 1,
+ crossPommy: 1,
+ crossGamma: 1,
+ crossArrowed: 1,
+ crossFitchy: 1,
+ crossCercelee: 1,
+ crossMoline: 2,
+ crossFourchy: 1,
+ crossAvellane: 1,
+ crossErminee: 1,
+ crossBiparted: 1,
+ crossMaltese: 3,
+ crossTemplar: 2,
+ crossCeltic: 1,
+ crossCeltic2: 1,
+ crossTriquetra: 1,
+ crossCarolingian: 1,
+ crossOccitan: 1,
+ crossSaltire: 3,
+ crossBurgundy: 1,
+ crossLatin: 3,
+ crossPatriarchal: 1,
+ crossOrthodox: 1,
+ crossCalvary: 1,
+ crossDouble: 1,
+ crossTau: 1,
+ crossSantiago: 1,
+ crossAnkh: 1
+ },
+ beasts: {
+ agnusDei: 1,
+ badgerStatant: 1,
+ bearPassant: 1,
+ bearRampant: 3,
+ boarRampant: 1,
+ bullPassant: 1,
+ camel: 1,
+ catPassantGuardant: 1,
+ cowStatant: 1,
+ dolphin: 1,
+ elephant: 1,
+ goat: 1,
+ greyhoundCourant: 1,
+ greyhoundRampant: 1,
+ greyhoundSejant: 1,
+ hedgehog: 1,
+ hindStatant: 1,
+ horsePassant: 1,
+ horseRampant: 2,
+ horseSalient: 1,
+ lamb: 1,
+ lambPassantReguardant: 1,
+ lionPassant: 3,
+ lionPassantGuardant: 2,
+ lionRampant: 7,
+ lionSejant: 2,
+ martenCourant: 1,
+ mastiffStatant: 1,
+ porcupine: 1,
+ rabbitSejant: 1,
+ ramPassant: 1,
+ ratRampant: 1,
+ rhinoceros: 1,
+ squirrel: 1,
+ stagLodgedRegardant: 1,
+ stagPassant: 1,
+ talbotPassant: 1,
+ talbotSejant: 1,
+ wolfPassant: 1,
+ wolfRampant: 1,
+ wolfStatant: 1
+ },
+ beastHeads: {
+ boarHeadErased: 1,
+ bullHeadCaboshed: 1,
+ deerHeadCaboshed: 1,
+ donkeyHeadCaboshed: 1,
+ elephantHeadErased: 1,
+ horseHeadCouped: 1,
+ lionHeadCaboshed: 2,
+ lionHeadErased: 2,
+ ramHeadErased: 1,
+ wolfHeadErased: 2
+ },
+ birds: {
+ cock: 3,
+ dove: 2,
+ doveDisplayed: 1,
+ duck: 1,
+ eagle: 9,
+ falcon: 2,
+ heron: 1,
+ owl: 1,
+ owlDisplayed: 1,
+ parrot: 1,
+ peacock: 1,
+ peacockInPride: 1,
+ raven: 2,
+ swallow: 1,
+ swan: 2,
+ swanErased: 1
+ },
+ reptiles: {
+ crocodile: 1,
+ frog: 1,
+ lizard: 1,
+ ouroboros: 1,
+ snake: 1
+ },
+ bugs: {
+ bee: 1,
+ butterfly: 1,
+ cancer: 1,
+ dragonfly: 1,
+ fly: 1,
+ ladybird: 1,
+ scorpion: 1,
+ wasp: 1
+ },
+ fishes: {
+ pike: 1,
+ plaice: 1,
+ salmon: 1
+ },
+ molluscs: {
+ escallop: 4,
+ snail: 1
+ },
+ plants: {
+ apple: 1,
+ cinquefoil: 1,
+ earOfWheat: 1,
+ grapeBunch: 1,
+ grapeBunch2: 1,
+ mapleLeaf: 1,
+ oak: 1,
+ palmTree: 1,
+ pear: 1,
+ pineCone: 1,
+ pineTree: 1,
+ quatrefoil: 1,
+ rose: 1,
+ sextifoil: 1,
+ thistle: 1,
+ tree: 1,
+ trefoil: 1,
+ wheatStalk: 1
+ },
+ fantastic: {
+ angel: 3,
+ basilisk: 1,
+ centaur: 1,
+ dragonPassant: 3,
+ dragonRampant: 2,
+ eagleTwoHeads: 2,
+ griffinPassant: 1,
+ griffinRampant: 2,
+ pegasus: 1,
+ sagittarius: 1,
+ serpent: 1,
+ unicornRampant: 1,
+ wyvern: 1,
+ wyvernWithWingsDisplayed: 1
+ },
+ agriculture: {
+ garb: 2,
+ millstone: 1,
+ plough: 1,
+ ploughshare: 1,
+ rake: 1,
+ scythe: 1,
+ scythe2: 1,
+ sickle: 1
+ },
+ arms: {
+ arbalest: 1,
+ arbalest2: 1,
+ arrow: 1,
+ arrowsSheaf: 1,
+ axe: 3,
+ bow: 1,
+ bowWithArrow: 2,
+ bowWithThreeArrows: 1,
+ cannon: 1,
+ falchion: 1,
+ flamberge: 1,
+ flangedMace: 1,
+ gauntlet: 1,
+ grenade: 1,
+ hatchet: 3,
+ helmet: 2,
+ helmetCorinthian: 1,
+ helmetGreat: 2,
+ helmetZischagge: 1,
+ lanceHead: 1,
+ lanceWithBanner: 1,
+ lochaberAxe: 1,
+ mace: 1,
+ maces: 1,
+ mallet: 1,
+ rapier: 1,
+ sabre: 1,
+ sabre2: 1,
+ sabresCrossed: 1,
+ shield: 1,
+ spear: 1,
+ sword: 4
+ },
+ bodyparts: {
+ armEmbowedHoldingSabre: 1,
+ armEmbowedVambraced: 1,
+ armEmbowedVambracedHoldingSword: 1,
+ bone: 1,
+ crossedBones: 2,
+ foot: 1,
+ hand: 4,
+ head: 1,
+ headWreathed: 1,
+ skeleton: 2,
+ skull: 2,
+ skull2: 1
+ },
+ people: {
+ archer: 1,
+ cavalier: 3,
+ cossack: 1,
+ monk: 1
+ },
+ architecture: {
+ bridge: 1,
+ bridge2: 1,
+ castle: 2,
+ castle2: 1,
+ column: 1,
+ lighthouse: 1,
+ palace: 1,
+ pillar: 1,
+ portcullis: 1,
+ tower: 2,
+ windmill: 1
+ },
+ seafaring: {
+ anchor: 6,
+ armillarySphere: 1,
+ boat: 2,
+ boat2: 1,
+ caravel: 1,
+ drakkar: 1,
+ lymphad: 2,
+ raft: 1,
+ shipWheel: 1
+ },
+ tools: {
+ anvil: 2,
+ drawingCompass: 2,
+ fan: 1,
+ hook: 1,
+ ladder: 1,
+ ladder2: 1,
+ pincers: 1,
+ saw: 1,
+ scale: 1,
+ scaleImbalanced: 1,
+ scalesHanging: 1,
+ scissors: 1,
+ scissors2: 1,
+ shears: 1,
+ trowel: 1
+ },
+ miscellaneous: {
+ attire: 2,
+ banner: 2,
+ bell: 3,
+ bookClosed: 1,
+ bookClosed2: 1,
+ bookOpen: 1,
+ bucket: 1,
+ buckle: 1,
+ bugleHorn: 2,
+ bugleHorn2: 1,
+ chain: 2,
+ chalice: 2,
+ cowHorns: 3,
+ crosier: 1,
+ crown: 3,
+ crown2: 2,
+ drum: 1,
+ fasces: 1,
+ feather: 3,
+ harp: 2,
+ horseshoe: 3,
+ hourglass: 2,
+ key: 3,
+ laurelWreath: 2,
+ laurelWreath2: 1,
+ log: 1,
+ lute: 2,
+ lyre: 1,
+ mitre: 1,
+ orb: 1,
+ pot: 2,
+ ramsHorn: 1,
+ sceptre: 1,
+ scrollClosed: 1,
+ snowflake: 1,
+ stagsAttires: 1,
+ stirrup: 2,
+ wheel: 3,
+ wing: 2,
+ wingSword: 1
+ },
+ inescutcheon: {
+ inescutcheonHeater: 1,
+ inescutcheonSpanish: 1,
+ inescutcheonFrench: 1,
+ inescutcheonHorsehead: 1,
+ inescutcheonHorsehead2: 1,
+ inescutcheonPolish: 1,
+ inescutcheonHessen: 1,
+ inescutcheonSwiss: 1,
+ inescutcheonBoeotian: 1,
+ inescutcheonRoman: 1,
+ inescutcheonKite: 1,
+ inescutcheonOldFrench: 1,
+ inescutcheonRenaissance: 1,
+ inescutcheonBaroque: 1,
+ inescutcheonTarge: 1,
+ inescutcheonTarge2: 1,
+ inescutcheonPavise: 1,
+ inescutcheonWedged: 1,
+ inescutcheonFlag: 1,
+ inescutcheonPennon: 1,
+ inescutcheonGuidon: 1,
+ inescutcheonBanner: 1,
+ inescutcheonDovetail: 1,
+ inescutcheonGonfalon: 1,
+ inescutcheonPennant: 1,
+ inescutcheonRound: 1,
+ inescutcheonOval: 1,
+ inescutcheonVesicaPiscis: 1,
+ inescutcheonSquare: 1,
+ inescutcheonDiamond: 1,
+ inescutcheonNo: 1,
+ inescutcheonFantasy1: 1,
+ inescutcheonFantasy2: 1,
+ inescutcheonFantasy3: 1,
+ inescutcheonFantasy4: 1,
+ inescutcheonFantasy5: 1,
+ inescutcheonNoldor: 1,
+ inescutcheonGondor: 1,
+ inescutcheonEasterling: 1,
+ inescutcheonErebor: 1,
+ inescutcheonIronHills: 1,
+ inescutcheonUrukHai: 1,
+ inescutcheonMoriaOrc: 1
+ },
+ ornaments: {
+ mantle: 0,
+ ribbon1: 3,
+ ribbon2: 2,
+ ribbon3: 1,
+ ribbon4: 1,
+ ribbon5: 1,
+ ribbon6: 1,
+ ribbon7: 1,
+ ribbon8: 1
+ },
+ data: chargeData
+ };
+
+ // charges specific to culture or burg type (FMG-only config, not coming from Armoria)
+ const typeMapping = {
+ Naval: {
+ anchor: 3,
+ drakkar: 1,
+ lymphad: 2,
+ caravel: 1,
+ shipWheel: 1,
+ armillarySphere: 1,
+ escallop: 1,
+ dolphin: 1,
+ plaice: 1
+ },
+ Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1},
+ River: {
+ garb: 1,
+ rake: 1,
+ raft: 1,
+ boat: 2,
+ drakkar: 2,
+ hook: 2,
+ pike: 2,
+ bullHeadCaboshed: 1,
+ apple: 1,
+ pear: 1,
+ plough: 1,
+ earOfWheat: 1,
+ salmon: 1,
+ cancer: 1,
+ bridge: 1,
+ bridge2: 2,
+ sickle: 1,
+ scythe: 1,
+ grapeBunch: 1,
+ wheatStalk: 1,
+ windmill: 1,
+ crocodile: 1
+ },
+ Lake: {
+ hook: 3,
+ cancer: 2,
+ escallop: 1,
+ pike: 2,
+ heron: 1,
+ boat: 1,
+ boat2: 2,
+ salmon: 1,
+ cancer: 1,
+ sickle: 1,
+ windmill: 1,
+ swanErased: 1,
+ swan: 1,
+ frog: 1,
+ wasp: 1
+ },
+ Nomadic: {
+ pot: 1,
+ buckle: 1,
+ wheel: 2,
+ sabre: 2,
+ sabresCrossed: 1,
+ bow: 2,
+ arrow: 1,
+ horseRampant: 1,
+ horseSalient: 1,
+ crescent: 1,
+ camel: 3,
+ scorpion: 1,
+ falcon: 1
+ },
+ Hunting: {
+ bugleHorn: 2,
+ bugleHorn2: 1,
+ stagsAttires: 2,
+ attire: 2,
+ hatchet: 1,
+ bowWithArrow: 2,
+ arrowsSheaf: 1,
+ lanceHead: 1,
+ saw: 1,
+ deerHeadCaboshed: 1,
+ wolfStatant: 1,
+ oak: 1,
+ pineCone: 1,
+ pineTree: 1,
+ oak: 1,
+ owl: 1,
+ falcon: 1,
+ peacock: 1,
+ boarHeadErased: 2,
+ horseHeadCouped: 1,
+ rabbitSejant: 1,
+ wolfRampant: 1,
+ wolfPassant: 1,
+ wolfStatant: 1,
+ greyhoundCourant: 1,
+ greyhoundRampant: 1,
+ greyhoundSejant: 1,
+ mastiffStatant: 1,
+ talbotPassant: 1,
+ talbotSejant: 1,
+ stagPassant: 21
+ },
+ // selection based on type
+ City: {
+ key: 4,
+ bell: 3,
+ lute: 1,
+ tower: 1,
+ pillar: 1,
+ castle: 1,
+ castle2: 1,
+ portcullis: 1,
+ mallet: 1,
+ cannon: 1,
+ anvil: 1,
+ buckle: 1,
+ horseshoe: 1,
+ stirrup: 1,
+ lanceWithBanner: 1,
+ bookClosed: 1,
+ scissors: 1,
+ scissors2: 1,
+ shears: 1,
+ pincers: 1,
+ bridge: 2,
+ archer: 1,
+ cannon: 1,
+ shield: 1,
+ arbalest: 1,
+ arbalest2: 1,
+ bowWithThreeArrows: 1,
+ spear: 1,
+ lochaberAxe: 1,
+ armEmbowedHoldingSabre: 1,
+ grenade: 1,
+ maces: 1,
+ grapeBunch: 1,
+ cock: 1,
+ ramHeadErased: 1,
+ ratRampant: 1,
+ hourglass: 1,
+ scale: 1,
+ scrollClosed: 1
+ },
+ Capital: {
+ crown: 2,
+ crown2: 2,
+ laurelWreath: 1,
+ orb: 1,
+ lute: 1,
+ lyre: 1,
+ banner: 1,
+ castle: 1,
+ castle2: 1,
+ palace: 1,
+ crown2: 2,
+ column: 1,
+ lionRampant: 1,
+ stagLodgedRegardant: 1,
+ drawingCompass: 1,
+ rapier: 1,
+ scaleImbalanced: 1,
+ scalesHanging: 1
+ },
+ Сathedra: {
+ crossHummetty: 3,
+ mitre: 3,
+ chalice: 1,
+ orb: 1,
+ crosier: 2,
+ lamb: 1,
+ monk: 2,
+ angel: 3,
+ crossLatin: 2,
+ crossPatriarchal: 1,
+ crossOrthodox: 1,
+ crossCalvary: 1,
+ agnusDei: 3,
+ bookOpen: 1,
+ sceptre: 1,
+ bone: 1,
+ skull: 1
+ }
+ };
+
+ const positions = {
+ conventional: {
+ e: 20,
+ abcdefgzi: 3,
+ beh: 3,
+ behdf: 2,
+ acegi: 1,
+ kn: 3,
+ bhdf: 1,
+ jeo: 1,
+ abc: 3,
+ jln: 6,
+ jlh: 3,
+ kmo: 2,
+ jleh: 1,
+ def: 3,
+ abcpqh: 4,
+ ABCDEFGHIJKL: 1
+ },
+ complex: {e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1},
+ divisions: {
+ perPale: {e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1},
+ perFess: {e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1},
+ perBend: {e: 5, lm: 5, bcfdgh: 1},
+ perBendSinister: {e: 1, jo: 1},
+ perCross: {e: 4, jlmo: 1, j: 1, jo: 2, jl: 1},
+ perChevron: {e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1},
+ perChevronReversed: {e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1},
+ perSaltire: {bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1},
+ perPile: {ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1}
+ },
+ inescutcheon: {e: 4, jln: 1}
+ };
+
+ const lines = {
+ straight: 50,
+ wavy: 8,
+ engrailed: 4,
+ invecked: 3,
+ rayonne: 3,
+ embattled: 1,
+ raguly: 1,
+ urdy: 1,
+ dancetty: 1,
+ indented: 2,
+ dentilly: 1,
+ bevilled: 1,
+ angled: 1,
+ flechy: 1,
+ barby: 1,
+ enclavy: 1,
+ escartely: 1,
+ arched: 2,
+ archedReversed: 1,
+ nowy: 1,
+ nowyReversed: 1,
+ embattledGhibellin: 1,
+ embattledNotched: 1,
+ embattledGrady: 1,
+ dovetailedIndented: 1,
+ dovetailed: 1,
+ potenty: 1,
+ potentyDexter: 1,
+ potentySinister: 1,
+ nebuly: 2,
+ seaWaves: 1,
+ dragonTeeth: 1,
+ firTrees: 1
+ };
+
+ const divisions = {
+ variants: {
+ perPale: 5,
+ perFess: 5,
+ perBend: 2,
+ perBendSinister: 1,
+ perChevron: 1,
+ perChevronReversed: 1,
+ perCross: 5,
+ perPile: 1,
+ perSaltire: 1,
+ gyronny: 1,
+ chevronny: 1
+ },
+ perPale: lines,
+ perFess: lines,
+ perBend: lines,
+ perBendSinister: lines,
+ perChevron: lines,
+ perChevronReversed: lines,
+ perCross: {
+ straight: 20,
+ wavy: 5,
+ engrailed: 4,
+ invecked: 3,
+ rayonne: 1,
+ embattled: 1,
+ raguly: 1,
+ urdy: 1,
+ indented: 2,
+ dentilly: 1,
+ bevilled: 1,
+ angled: 1,
+ embattledGhibellin: 1,
+ embattledGrady: 1,
+ dovetailedIndented: 1,
+ dovetailed: 1,
+ potenty: 1,
+ potentyDexter: 1,
+ potentySinister: 1,
+ nebuly: 1
+ },
+ perPile: lines
+ };
+
+ const ordinaries = {
+ lined: {
+ pale: 7,
+ fess: 5,
+ bend: 3,
+ bendSinister: 2,
+ chief: 5,
+ bar: 2,
+ gemelle: 1,
+ fessCotissed: 1,
+ fessDoubleCotissed: 1,
+ bendlet: 2,
+ bendletSinister: 1,
+ terrace: 3,
+ cross: 6,
+ crossParted: 1,
+ saltire: 2,
+ saltireParted: 1
+ },
+ straight: {
+ bordure: 8,
+ orle: 4,
+ mount: 1,
+ point: 2,
+ flaunches: 1,
+ gore: 1,
+ gyron: 1,
+ quarter: 1,
+ canton: 2,
+ pall: 3,
+ pallReversed: 2,
+ chevron: 4,
+ chevronReversed: 3,
+ pile: 2,
+ pileInBend: 2,
+ pileInBendSinister: 1,
+ piles: 1,
+ pilesInPoint: 2,
+ label: 1
+ },
+ data: {
+ bar: {
+ positionsOn: {defdefdef: 1},
+ positionsOff: {abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1}
+ },
+ bend: {
+ positionsOn: {ee: 2, jo: 1, joe: 1},
+ positionsOff: {ccg: 2, ccc: 1}
+ },
+ bendSinister: {
+ positionsOn: {ee: 1, lm: 1, lem: 4},
+ positionsOff: {aai: 2, aaa: 1}
+ },
+ bendlet: {
+ positionsOn: {joejoejoe: 1},
+ positionsOff: {ccg: 2, ccc: 1}
+ },
+ bendletSinister: {
+ positionsOn: {lemlemlem: 1},
+ positionsOff: {aai: 2, aaa: 1}
+ },
+ bordure: {
+ positionsOn: {ABCDEFGHIJKL: 1},
+ positionsOff: {e: 4, jleh: 2, kenken: 1, peqpeq: 1}
+ },
+ canton: {
+ positionsOn: {yyyy: 1},
+ positionsOff: {e: 5, beh: 1, def: 1, bdefh: 1, kn: 1}
+ },
+ chevron: {
+ positionsOn: {ach: 3, hhh: 1}
+ },
+ chevronReversed: {
+ positionsOff: {bbb: 1}
+ },
+ chief: {
+ positionsOn: {abc: 5, bbb: 1},
+ positionsOff: {emo: 2, emoz: 1, ez: 2}
+ },
+ cross: {
+ positionsOn: {eeee: 1, behdfbehdf: 3, behbehbeh: 2},
+ positionsOff: {acgi: 1}
+ },
+ crossParted: {
+ positionsOn: {e: 5, ee: 1}
+ },
+ fess: {
+ positionsOn: {ee: 1, def: 3},
+ positionsOff: {abc: 3, abcz: 1}
+ },
+ fessCotissed: {
+ positionsOn: {ee: 1, def: 3}
+ },
+ fessDoubleCotissed: {
+ positionsOn: {ee: 1, defdef: 3}
+ },
+ flaunches: {
+ positionsOff: {e: 3, kn: 1, beh: 3}
+ },
+ gemelle: {
+ positionsOff: {abc: 1}
+ },
+ gyron: {
+ positionsOff: {bh: 1}
+ },
+ label: {
+ positionsOff: {defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5}
+ },
+ mount: {
+ positionsOff: {e: 5, def: 1, bdf: 3}
+ },
+ orle: {
+ positionsOff: {e: 4, jleh: 1, kenken: 1, peqpeq: 1}
+ },
+ pale: {
+ positionsOn: {ee: 12, beh: 10, kn: 3, bb: 1},
+ positionsOff: {yyy: 1}
+ },
+ pall: {
+ positionsOn: {ee: 1, jleh: 5, jlhh: 3},
+ positionsOff: {BCKFEILGJbdmfo: 1}
+ },
+ pallReversed: {
+ positionsOn: {ee: 1, bemo: 5},
+ positionsOff: {aczac: 1}
+ },
+ pile: {
+ positionsOn: {bbb: 1},
+ positionsOff: {acdfgi: 1, acac: 1}
+ },
+ pileInBend: {
+ positionsOn: {eeee: 1, eeoo: 1},
+ positionsOff: {cg: 1}
+ },
+ pileInBendSinister: {
+ positionsOn: {eeee: 1, eemm: 1},
+ positionsOff: {ai: 1}
+ },
+ point: {
+ positionsOff: {e: 2, def: 1, bdf: 3, acbdef: 1}
+ },
+ quarter: {
+ positionsOn: {jjj: 1},
+ positionsOff: {e: 1}
+ },
+ saltire: {
+ positionsOn: {ee: 5, jlemo: 1}
+ },
+ saltireParted: {
+ positionsOn: {e: 5, ee: 1}
+ },
+ terrace: {
+ positionsOff: {e: 5, def: 1, bdf: 3}
+ }
+ }
+ };
+
+ const shields = {
+ types: {basic: 10, regional: 2, historical: 1, specific: 1, banner: 1, simple: 2, fantasy: 1, middleEarth: 0},
+ basic: {heater: 12, spanish: 6, french: 1},
+ regional: {horsehead: 1, horsehead2: 1, polish: 1, hessen: 1, swiss: 1},
+ historical: {boeotian: 1, roman: 2, kite: 1, oldFrench: 5, renaissance: 2, baroque: 2},
+ specific: {targe: 1, targe2: 0, pavise: 5, wedged: 10},
+ banner: {flag: 1, pennon: 0, guidon: 0, banner: 0, dovetail: 1, gonfalon: 5, pennant: 0},
+ simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
+ fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3},
+ middleEarth: {noldor: 1, gondor: 1, easterling: 1, erebor: 1, ironHills: 1, urukHai: 1, moriaOrc: 1}
+ };
+
+ const generate = function (parent, kinship, dominion, type) {
+ if (!parent || parent.custom) {
+ parent = null;
+ kinship = 0;
+ dominion = 0;
+ }
+
+ let usedPattern = null;
+ let usedTinctures = [];
+
+ const t1 = P(kinship) ? parent.t1 : getTincture("field");
+ if (t1.includes("-")) usedPattern = t1;
+ const coa = {t1};
+
+ const addCharge = P(usedPattern ? 0.5 : 0.93); // 80% for charge
+ const linedOrdinary =
+ (addCharge && P(0.3)) || P(0.5)
+ ? parent?.ordinaries && P(kinship)
+ ? parent.ordinaries[0].ordinary
+ : rw(ordinaries.lined)
+ : null;
+
+ const ordinary =
+ (!addCharge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
+
+ const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
+
+ const divisioned = (() => {
+ if (rareDivided) return P(0.03);
+ if (addCharge && ordinary) return P(0.03);
+ if (addCharge) return P(0.3);
+ if (ordinary) return P(0.7);
+ return P(0.995);
+ })();
+
+ const division = (() => {
+ if (divisioned) {
+ if (parent?.division && P(kinship - 0.1)) return parent.division.division;
+ return rw(divisions.variants);
+ }
+ return null;
+ })();
+
+ if (division) {
+ const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
+ coa.division = {division, t};
+ if (divisions[division])
+ coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
+ }
+
+ if (ordinary) {
+ coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}];
+ if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? "straight" : rw(lines);
+ if (division && !addCharge && !usedPattern && P(0.5) && ordinary !== "bordure" && ordinary !== "orle") {
+ if (P(0.8)) coa.ordinaries[0].divided = "counter";
+ // 40%
+ else if (P(0.6)) coa.ordinaries[0].divided = "field";
+ // 6%
+ else coa.ordinaries[0].divided = "division"; // 4%
+ }
+ }
+
+ if (addCharge) {
+ const charge = (() => {
+ if (parent?.charges && P(kinship - 0.1)) return parent.charges[0].charge;
+ if (type && type !== "Generic" && P(0.3)) return rw(typeMapping[type]);
+ return selectCharge(ordinary || divisioned ? charges.types : charges.single);
+ })();
+ const chargeData = charges.data[charge] || {};
+
+ let p = "e";
+ let t = "gules";
+
+ const ordinaryData = ordinaries.data[ordinary];
+ const tOrdinary = coa.ordinaries ? coa.ordinaries[0].t : null;
+
+ if (ordinaryData?.positionsOn && P(0.8)) {
+ // place charge over ordinary (use tincture of field type)
+ p = rw(ordinaryData.positionsOn);
+ t = !usedPattern && P(0.3) ? coa.t1 : getTincture("charge", [], tOrdinary);
+ } else if (ordinaryData?.positionsOff && P(0.95)) {
+ // place charge out of ordinary (use tincture of ordinary type)
+ p = rw(ordinaryData.positionsOff);
+ t = !usedPattern && P(0.3) ? tOrdinary : getTincture("charge", usedTinctures, coa.t1);
+ } else if (positions.divisions[division]) {
+ // place charge in fields made by division
+ p = rw(positions.divisions[division]);
+ t = getTincture("charge", tOrdinary ? usedTinctures.concat(tOrdinary) : usedTinctures, coa.t1);
+ } else if (chargeData.positions) {
+ // place charge-suitable position
+ p = rw(chargeData.positions);
+ t = getTincture("charge", usedTinctures, coa.t1);
+ } else {
+ // place in standard position (use new tincture)
+ p = usedPattern ? "e" : charges.conventional[charge] ? rw(positions.conventional) : rw(positions.complex);
+ t = getTincture("charge", usedTinctures.concat(tOrdinary), coa.t1);
+ }
+
+ if (chargeData.natural && chargeData.natural !== t && chargeData.natural !== tOrdinary) t = chargeData.natural;
+
+ const item = {charge: charge, t, p};
+ const colors = chargeData.colors || 1;
+ if (colors > 1) item.t2 = P(0.25) ? getTincture("charge", usedTinctures, coa.t1) : t;
+ if (colors > 2 && item.t2) item.t3 = P(0.5) ? getTincture("charge", usedTinctures, coa.t1) : t;
+ coa.charges = [item];
+
+ if (p === "ABCDEFGHIKL" && P(0.95)) {
+ // add central charge if charge is in bordure
+ coa.charges[0].charge = rw(charges.conventional);
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.t1);
+ coa.charges.push({charge, t, p: "e"});
+ } else if (P(0.8) && charge === "inescutcheon") {
+ // add charge to inescutcheon
+ const charge = selectCharge(charges.types);
+ const t2 = getTincture("charge", [], t);
+ coa.charges.push({charge, t: t2, p, size: 0.5});
+ } else if (division && !ordinary) {
+ const allowCounter = !usedPattern && (!coa.line || coa.line === "straight");
+
+ // dimidiation: second charge at division basic positons
+ if (P(0.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") {
+ coa.charges[0].divided = "field";
+ if (P(0.95)) {
+ const p2 = p === "e" || P(0.5) ? "e" : rw(positions.divisions[division]);
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.division.t);
+ coa.charges.push({charge, t, p: p2, divided: "division"});
+ }
+ } else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
+ // counterchanged, 40%
+ else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
+ // place 2 charges in division standard positions
+ const [p1, p2] =
+ division === "perPale"
+ ? ["p", "q"]
+ : division === "perFess"
+ ? ["k", "n"]
+ : division === "perBend"
+ ? ["l", "m"]
+ : ["j", "o"]; // perBendSinister
+ coa.charges[0].p = p1;
+
+ const charge = selectCharge(charges.single);
+ const t = getTincture("charge", usedTinctures, coa.division.t);
+ coa.charges.push({charge, t, p: p2});
+ } else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
+ // place 4 charges in division standard positions
+ const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"];
+ coa.charges[0].p = p1;
+
+ const c2 = selectCharge(charges.single);
+ const t2 = getTincture("charge", [], coa.division.t);
+
+ const c3 = selectCharge(charges.single);
+ const t3 = getTincture("charge", [], coa.division.t);
+
+ const c4 = selectCharge(charges.single);
+ const t4 = getTincture("charge", [], coa.t1);
+ coa.charges.push({charge: c2, t: t2, p: p2}, {charge: c3, t: t3, p: p3}, {charge: c4, t: t4, p: p4});
+ } else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40%
+ }
+
+ coa.charges.forEach(c => defineChargeAttributes(ordinary, division, c));
+ }
+
+ // dominions have canton with parent coa
+ if (P(dominion) && parent.charges) {
+ const invert = isSameType(parent.t1, coa.t1);
+ const t = invert ? getTincture("division", usedTinctures, coa.t1) : parent.t1;
+ const canton = {ordinary: "canton", t};
+
+ coa.charges?.forEach((charge, i) => {
+ if (charge.size === 1.5) charge.size = 1.4;
+ charge.p = charge.p.replaceAll(/[ajy]/g, "");
+ if (!charge.p) coa.charges.splice(i, 1);
+ });
+
+ let charge = parent.charges[0].charge;
+ if (charge === "inescutcheon" && parent.charges[1]) charge = parent.charges[1].charge;
+
+ let t2 = invert ? parent.t1 : parent.charges[0].t;
+ if (isSameType(t, t2)) t2 = getTincture("charge", usedTinctures, t);
+
+ if (!coa.charges) coa.charges = [];
+ coa.charges.push({charge, t: t2, p: "y", size: 0.5});
+
+ coa.ordinaries ? coa.ordinaries.push(canton) : (coa.ordinaries = [canton]);
+ }
+
+ function selectCharge(set) {
+ const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types) : rw(charges.single);
+ return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]);
+ }
+
+ // select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
+ function getTincture(element, fields = [], RoT) {
+ const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
+
+ let type = rw(tinctures[element]); // metals, colours, stains, patterns
+ if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT
+ if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse
+ let tincture = rw(tinctures[type]);
+
+ while (tincture === base || fields.includes(tincture)) {
+ tincture = rw(tinctures[type]);
+ } // follow RoT
+
+ if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture
+
+ if (type === "patterns") {
+ usedPattern = tincture;
+ tincture = definePattern(tincture, element);
+ }
+
+ return tincture;
+ }
+
+ function defineChargeAttributes(ordinary, division, c) {
+ // define size
+ c.size = (c.size || 1) * getSize(c.p, ordinary, division);
+
+ // clean-up position
+ c.p = [...new Set(c.p)].join("");
+
+ // define orientation
+ if (P(0.02) && charges.data[c.charge]?.sinister) c.sinister = 1;
+ if (P(0.02) && charges.data[c.charge]?.reversed) c.reversed = 1;
+ }
+
+ function getType(t) {
+ const tincture = t.includes("-") ? t.split("-")[1] : t;
+ if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
+ if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
+ if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
+ }
+
+ function isSameType(t1, t2) {
+ return type(t1) === type(t2);
+
+ function type(tincture) {
+ if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
+ if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
+ if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
+ else return "pattern";
+ }
+ }
+
+ function definePattern(pattern, element, size = "") {
+ let t1 = null,
+ t2 = null;
+ if (P(0.1)) size = "-small";
+ else if (P(0.1)) size = "-smaller";
+ else if (P(0.01)) size = "-big";
+ else if (P(0.005)) size = "-smallest";
+
+ // apply standard tinctures
+ if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
+ t1 = "azure";
+ t2 = "argent";
+ } else if (P(0.8) && pattern === "ermine") {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (pattern === "pappellony") {
+ if (P(0.2)) {
+ t1 = "gules";
+ t2 = "or";
+ } else if (P(0.2)) {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (P(0.2)) {
+ t1 = "azure";
+ t2 = "argent";
+ }
+ } else if (pattern === "masoned") {
+ if (P(0.3)) {
+ t1 = "gules";
+ t2 = "argent";
+ } else if (P(0.3)) {
+ t1 = "argent";
+ t2 = "sable";
+ } else if (P(0.1)) {
+ t1 = "or";
+ t2 = "sable";
+ }
+ } else if (pattern === "fretty") {
+ if (t2 === "sable" || P(0.35)) {
+ t1 = "argent";
+ t2 = "gules";
+ } else if (P(0.25)) {
+ t1 = "sable";
+ t2 = "or";
+ } else if (P(0.15)) {
+ t1 = "gules";
+ t2 = "argent";
+ }
+ } else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
+
+ if (!t1 || !t2) {
+ const startWithMetal = P(0.7);
+ t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
+ t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
+ }
+
+ // division should not be the same tincture as base field
+ if (element === "division") {
+ if (usedTinctures.includes(t1)) t1 = replaceTincture(t1);
+ if (usedTinctures.includes(t2)) t2 = replaceTincture(t2);
+ }
+
+ usedTinctures.push(t1, t2);
+ return `${pattern}-${t1}-${t2}${size}`;
+ }
+
+ function replaceTincture(t, n) {
+ const type = getType(t);
+ while (!n || n === t) {
+ n = rw(tinctures[type]);
+ }
+ return n;
+ }
+
+ function getSize(p, o = null, d = null) {
+ if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
+ if (p === "e") return 1.5;
+ if (p === "jln" || p === "jlh") return 0.7;
+ if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
+ if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return 0.5;
+ if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return 0.6;
+ if (p.length > 10) return 0.18; // >10 (bordure)
+ if (p.length > 7) return 0.3; // 8, 9, 10
+ if (p.length > 4) return 0.4; // 5, 6, 7
+ if (p.length > 2) return 0.5; // 3, 4
+ return 0.7; // 1, 2
+ }
+
+ return coa;
+ };
+
+ const getShield = function (culture, state) {
+ const emblemShape = document.getElementById("emblemShape");
+ const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform";
+ if (shapeGroup !== "Diversiform") return emblemShape.value;
+
+ if (emblemShape.value === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
+ if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
+ ERROR && console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
+ return "heater";
+ };
+
+ const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
+ const copy = coa => JSON.parse(JSON.stringify(coa));
+
+ return {generate, toString, copy, getShield, shields};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./coa-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./coa-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in coa-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into coa-generator_render.md
diff --git a/procedural/src/engine/support/coa-generator_render.md b/procedural/src/engine/support/coa-generator_render.md
new file mode 100644
index 00000000..11c60fdd
--- /dev/null
+++ b/procedural/src/engine/support/coa-generator_render.md
@@ -0,0 +1,48 @@
+# Removed Rendering/UI Logic from coa-generator.js
+
+The following DOM manipulation and UI-related code blocks have been **removed** from the core engine module and should be moved to the Viewer application:
+
+## DOM Element Access - Lines 2269-2271
+
+**Removed Code:**
+```javascript
+const emblemShape = document.getElementById("emblemShape");
+const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform";
+if (shapeGroup !== "Diversiform") return emblemShape.value;
+```
+
+**Location**: Originally in `getShield()` function (lines 2269-2271)
+**Reason**: Direct DOM access via `document.getElementById()` and manipulation of select element options
+**Replacement**: These values should be read by the Viewer and passed via the `config` parameter
+
+## DOM Value Reading - Line 2273
+
+**Removed Code:**
+```javascript
+if (emblemShape.value === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
+```
+
+**Location**: Originally in `getShield()` function (line 2273)
+**Reason**: Direct access to DOM element `.value` property
+**Replacement**: The `emblemShape` value should be passed via `config.emblemShape`
+
+## Error Console Logging - Line 2275
+
+**Removed Code:**
+```javascript
+ERROR && console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
+```
+
+**Location**: Originally in `getShield()` function (line 2275)
+**Reason**: Global `ERROR` variable dependency and console error logging
+**Replacement**: Error handling should be implemented by the calling Viewer code
+
+## Summary
+
+All removed code was related to:
+1. **DOM Element Selection**: `document.getElementById("emblemShape")`
+2. **DOM Property Access**: `.selectedOptions[0]?.parentNode.label`, `.value`
+3. **Global Variable Dependencies**: `ERROR` variable
+4. **Direct Console Logging**: `console.error()` calls
+
+These UI concerns should now be handled by the Viewer application, which will read the DOM values and pass them to the core engine via the config parameter.
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-renderer_config.md b/procedural/src/engine/support/coa-renderer_config.md
new file mode 100644
index 00000000..b541a91a
--- /dev/null
+++ b/procedural/src/engine/support/coa-renderer_config.md
@@ -0,0 +1,6 @@
+# Config Properties for coa-renderer.js
+
+The refactored coa-renderer.js module is self-contained in terms of configuration and does not require any properties to be passed in a config object.
+
+Notes
+All necessary parameters and definitions are derived directly from the coa (Coat of Arms) object that is passed as a primary argument to the render function. This module has no dependency on UI settings or external configuration values.
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-renderer_external.md b/procedural/src/engine/support/coa-renderer_external.md
new file mode 100644
index 00000000..73541c1a
--- /dev/null
+++ b/procedural/src/engine/support/coa-renderer_external.md
@@ -0,0 +1,39 @@
+# coa-renderer_external.md
+
+External Dependencies for coa-renderer.js
+The refactored coa-renderer.js module has one critical external data dependency that must be provided by the calling environment.
+
+- Required Data Dependencies
+- chargesData
+- Type: Object
+- Description: An object that serves as a map between a charge's name and its raw SVG content. The engine no longer fetches these files itself; the Viewer/Client is responsible for loading them and passing them into the render function.
+Structure:
+
+```javascript
+{
+ "chargeName1": "... ", // The raw tag content of the charge's SVG
+ "chargeName2": "... ",
+ // etc.
+}
+```
+
+Example:
+
+```javascript
+const chargesData = {
+ "lion": ' ',
+ "eagle": ' '
+};
+```
+
+## Notes on Viewer Implementation
+
+The Viewer application is now responsible for the I/O operations previously handled by the engine. It must:
+
+- Identify all unique charges required for a set of Coats of Arms.
+- Fetch the corresponding SVG files (e.g., from a /charges/ directory).
+- Read the content of each file into a string.
+- Assemble the chargesData object.
+- Pass this object to the coa-renderer.render() function.
+
+This change ensures the core engine remains free of environment-specific APIs like fetch and is fully portable.
\ No newline at end of file
diff --git a/procedural/src/engine/support/coa-renderer_prompt.md b/procedural/src/engine/support/coa-renderer_prompt.md
new file mode 100644
index 00000000..02b46a76
--- /dev/null
+++ b/procedural/src/engine/support/coa-renderer_prompt.md
@@ -0,0 +1,2128 @@
+# coa-renderer.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `coa-renderer.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.COArenderer = (function () {
+ const colors = {
+ argent: "#fafafa",
+ or: "#ffe066",
+ gules: "#d7374a",
+ sable: "#333333",
+ azure: "#377cd7",
+ vert: "#26c061",
+ purpure: "#522d5b",
+ murrey: "#85185b",
+ sanguine: "#b63a3a",
+ tenné: "#cc7f19"
+ };
+
+ const shieldPositions = {
+ // shield-specific position: [x, y] (relative to center)
+ heater: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-32.25, 37.5],
+ h: [0, 50],
+ i: [32.25, 37.5],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-30, 30],
+ n: [0, 42.5],
+ o: [30, 30],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.2, -20],
+ E: [66.2, -20],
+ J: [-55.5, 26],
+ F: [55.5, 26],
+ I: [-33, 62],
+ G: [33, 62],
+ H: [0, 89.5]
+ },
+ spanish: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.4, -20],
+ E: [66.4, -20],
+ J: [-66.4, 26],
+ F: [66.4, 26],
+ I: [-49, 70],
+ G: [49, 70],
+ H: [0, 92]
+ },
+ french: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 65],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.4, -20],
+ E: [66.4, -20],
+ J: [-66.4, 26],
+ F: [66.4, 26],
+ I: [-65.4, 70],
+ G: [65.4, 70],
+ H: [0, 89]
+ },
+ horsehead: {
+ a: [-43.75, -47.5],
+ b: [0, -50],
+ c: [43.75, -47.5],
+ d: [-35, 0],
+ e: [0, 0],
+ f: [35, 0],
+ h: [0, 50],
+ y: [-50, -50],
+ z: [0, 55],
+ j: [-35, -35],
+ k: [0, -40],
+ l: [35, -35],
+ m: [-30, 30],
+ n: [0, 40],
+ o: [30, 30],
+ p: [-27.5, 0],
+ q: [27.5, 0],
+ A: [-71, -52],
+ B: [-24, -73],
+ C: [24, -73],
+ D: [71, -52],
+ K: [-62, -16],
+ E: [62, -16],
+ J: [-39, 20],
+ F: [39, 20],
+ I: [-33.5, 60],
+ G: [33.5, 60],
+ H: [0, 91.5]
+ },
+ horsehead2: {
+ a: [-37.5, -47.5],
+ b: [0, -50],
+ c: [37.5, -47.5],
+ d: [-35, 0],
+ e: [0, 0],
+ f: [35, 0],
+ g: [-35, 47.5],
+ h: [0, 50],
+ i: [35, 47.5],
+ y: [-50, -50],
+ z: [0, 55],
+ j: [-30, -30],
+ k: [0, -40],
+ l: [30, -30],
+ m: [-30, 30],
+ n: [0, 40],
+ o: [30, 30],
+ p: [-27.5, 0],
+ q: [27.5, 0],
+ A: [-49, -39],
+ B: [-22, -70],
+ C: [22, -70],
+ D: [49, -39],
+ K: [-51, -2],
+ E: [51, -2],
+ J: [-38.5, 31],
+ F: [38.5, 31],
+ I: [-35, 67],
+ G: [35, 67],
+ H: [0, 85]
+ },
+ polish: {
+ a: [-35, -50],
+ b: [0, -50],
+ c: [35, -50],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-37.5, 50],
+ h: [0, 50],
+ i: [37.5, 50],
+ y: [-50, -50],
+ z: [0, 65],
+ j: [-27.5, -27.5],
+ k: [0, -45],
+ l: [27.5, -27.5],
+ m: [-27.5, 27.5],
+ n: [0, 45],
+ o: [27.5, 27.5],
+ p: [-32.5, 0],
+ q: [32.5, 0],
+ A: [-48, -52],
+ B: [-23, -80],
+ C: [23, -80],
+ D: [48, -52],
+ K: [-47, -10],
+ E: [47, -10],
+ J: [-62, 32],
+ F: [62, 32],
+ I: [-37, 68],
+ G: [37, 68],
+ H: [0, 86]
+ },
+ hessen: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 52.5],
+ j: [-40, -40],
+ k: [0, -40],
+ l: [40, -40],
+ m: [-40, 40],
+ n: [0, 40],
+ o: [40, 40],
+ p: [-40, 0],
+ q: [40, 0],
+ A: [-69, -64],
+ B: [-22, -76],
+ C: [22, -76],
+ D: [69, -64],
+ K: [-66.4, -20],
+ E: [66.4, -20],
+ J: [-62, 26],
+ F: [62, 26],
+ I: [-46, 70],
+ G: [46, 70],
+ H: [0, 91.5]
+ },
+ swiss: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-32, 37.5],
+ h: [0, 50],
+ i: [32, 37.5],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-32, 32.5],
+ n: [0, 42.5],
+ o: [32, 32.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66],
+ C: [22, -66],
+ D: [66.2, -66.6],
+ K: [-63, -20],
+ E: [63, -20],
+ J: [-50, 26],
+ F: [50, 26],
+ I: [-29, 62],
+ G: [29, 62],
+ H: [0, 89.5]
+ },
+ boeotian: {
+ a: [-37.5, -47.5],
+ b: [0, -47.5],
+ c: [37.5, -47.5],
+ d: [-25, 0],
+ e: [0, 0],
+ f: [25, 0],
+ g: [-37.5, 47.5],
+ h: [0, 47.5],
+ i: [37.5, 47.5],
+ y: [-48, -48],
+ z: [0, 60],
+ j: [-32.5, -37.5],
+ k: [0, -45],
+ l: [32.5, -37.5],
+ m: [-32.5, 37.5],
+ n: [0, 45],
+ o: [32.5, 37.5],
+ p: [-20, 0],
+ q: [20, 0],
+ A: [-45, -55],
+ B: [-20, -77],
+ C: [20, -77],
+ D: [45, -55],
+ K: [-59, -25],
+ E: [59, -25],
+ J: [-58, 27],
+ F: [58, 27],
+ I: [-39, 63],
+ G: [39, 63],
+ H: [0, 81]
+ },
+ roman: {
+ a: [-40, -52.5],
+ b: [0, -52.5],
+ c: [40, -52.5],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-40, 52.5],
+ h: [0, 52.5],
+ i: [40, 52.5],
+ y: [-42.5, -52.5],
+ z: [0, 65],
+ j: [-30, -37.5],
+ k: [0, -37.5],
+ l: [30, -37.5],
+ m: [-30, 37.5],
+ n: [0, 37.5],
+ o: [30, 37.5],
+ p: [-30, 0],
+ q: [30, 0],
+ A: [-51.5, -65],
+ B: [-17, -75],
+ C: [17, -75],
+ D: [51.5, -65],
+ K: [-51.5, -21],
+ E: [51.5, -21],
+ J: [-51.5, 21],
+ F: [51.5, 21],
+ I: [-51.5, 65],
+ G: [51.5, 65],
+ H: [-17, 75],
+ L: [17, 75]
+ },
+ kite: {
+ b: [0, -65],
+ e: [0, -15],
+ h: [0, 35],
+ z: [0, 35],
+ k: [0, -50],
+ n: [0, 20],
+ p: [-20, -15],
+ q: [20, -15],
+ A: [-38, -52],
+ B: [-29, -78],
+ C: [29, -78],
+ D: [38, -52],
+ K: [-33, -20],
+ E: [33, -20],
+ J: [-25, 11],
+ F: [25, 11],
+ I: [-15, 42],
+ G: [15, 42],
+ H: [0, 73],
+ L: [0, -91]
+ },
+ oldFrench: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-37.5, 50],
+ h: [0, 50],
+ i: [37.5, 50],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 45],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.2, -66.6],
+ B: [-22, -66.6],
+ C: [22, -66.6],
+ D: [66.2, -66.6],
+ K: [-66.2, -20],
+ E: [66.2, -20],
+ J: [-64, 26],
+ F: [64, 26],
+ I: [-45, 62],
+ G: [45, 62],
+ H: [0, 91]
+ },
+ renaissance: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-41.5, 0],
+ e: [0, 0],
+ f: [41.5, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-61, -55],
+ B: [-23, -67],
+ C: [23, -67],
+ D: [61, -55],
+ K: [-55, -11],
+ E: [55, -11],
+ J: [-65, 31],
+ F: [65, 31],
+ I: [-45, 76],
+ G: [45, 76],
+ H: [0, 87]
+ },
+ baroque: {
+ a: [-43.75, -45],
+ b: [0, -45],
+ c: [43.75, -45],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 60],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-65, -54.5],
+ B: [-22, -65],
+ C: [22, -65],
+ D: [65, -54.5],
+ K: [-58.5, -15],
+ E: [58.5, -15],
+ J: [-65, 31],
+ F: [66, 31],
+ I: [-35, 73],
+ G: [35, 73],
+ H: [0, 89]
+ },
+ targe: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-40, -40],
+ k: [0, -40],
+ l: [40, -40],
+ m: [-40, 40],
+ n: [0, 40],
+ o: [40, 40],
+ p: [-32.5, 0],
+ q: [32.5, 0],
+ A: [-66.2, -60],
+ B: [-22, -77],
+ C: [22, -86],
+ D: [60, -66.6],
+ K: [-28, -20],
+ E: [57, -20],
+ J: [-61, 26],
+ F: [61, 26],
+ I: [-49, 63],
+ G: [49, 59],
+ H: [0, 80]
+ },
+ targe2: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-43.75, 50],
+ h: [0, 50],
+ i: [43.75, 50],
+ y: [-50, -50],
+ z: [0, 60],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-32.5, 0],
+ q: [32.5, 0],
+ A: [-55, -59],
+ B: [-15, -59],
+ C: [24, -79],
+ D: [51, -58],
+ K: [-40, -14],
+ E: [51, -14],
+ J: [-64, 26],
+ F: [62, 26],
+ I: [-46, 66],
+ G: [48, 67],
+ H: [0, 83]
+ },
+ pavise: {
+ a: [-40, -52.5],
+ b: [0, -52.5],
+ c: [40, -52.5],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-40, 52.5],
+ h: [0, 52.5],
+ i: [40, 52.5],
+ y: [-42.5, -52.5],
+ z: [0, 60],
+ j: [-30, -35],
+ k: [0, -37.5],
+ l: [30, -35],
+ m: [-30, 35],
+ n: [0, 37.5],
+ o: [30, 35],
+ p: [-30, 0],
+ q: [30, 0],
+ A: [-57, -55],
+ B: [-22, -74],
+ C: [22, -74],
+ D: [57, -55],
+ K: [-54, -11],
+ E: [54, -11],
+ J: [-50, 36],
+ F: [50, 36],
+ I: [-46, 81],
+ G: [46, 81],
+ H: [0, 81]
+ },
+ wedged: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.75, 0],
+ e: [0, 0],
+ f: [43.75, 0],
+ g: [-32.25, 37.5],
+ h: [0, 50],
+ i: [32.25, 37.5],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-32.5, 32.5],
+ n: [0, 42.5],
+ o: [32.5, 32.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66, -53],
+ B: [-22, -72.5],
+ C: [22, -72.5],
+ D: [66, -53],
+ K: [-62.6, -13],
+ E: [62.6, -13],
+ J: [-50, 26],
+ F: [50, 26],
+ I: [-27, 62],
+ G: [27, 62],
+ H: [0, 87]
+ },
+ flag: {
+ a: [-60, -40],
+ b: [0, -40],
+ c: [60, -40],
+ d: [-60, 0],
+ e: [0, 0],
+ f: [60, 0],
+ g: [-60, 40],
+ h: [0, 40],
+ i: [60, 40],
+ y: [-60, -42.5],
+ z: [0, 40],
+ j: [-45, -30],
+ k: [0, -30],
+ l: [45, -30],
+ m: [-45, 30],
+ n: [0, 30],
+ o: [45, 30],
+ p: [-45, 0],
+ q: [45, 0],
+ A: [-81, -51],
+ B: [-27, -51],
+ C: [27, -51],
+ D: [81, -51],
+ K: [-81, -17],
+ E: [81, -17],
+ J: [-81, 17],
+ F: [81, 17],
+ I: [-81, 51],
+ G: [81, 51],
+ H: [-27, 51],
+ L: [27, 51]
+ },
+ pennon: {
+ a: [-75, -40],
+ d: [-75, 0],
+ e: [-25, 0],
+ f: [25, 0],
+ g: [-75, 40],
+ y: [-70, -42.5],
+ j: [-60, -30],
+ m: [-60, 30],
+ p: [-60, 0],
+ q: [5, 0],
+ A: [-81, -48],
+ B: [-43, -36],
+ C: [-4.5, -24],
+ D: [33, -12],
+ E: [72, 0],
+ F: [33, 12],
+ G: [-4.5, 24],
+ H: [-43, 36],
+ I: [-81, 48],
+ J: [-81, 17],
+ K: [-81, -17]
+ },
+ guidon: {
+ a: [-60, -40],
+ b: [0, -40],
+ c: [60, -40],
+ d: [-60, 0],
+ e: [0, 0],
+ g: [-60, 40],
+ h: [0, 40],
+ i: [60, 40],
+ y: [-60, -42.5],
+ z: [0, 40],
+ j: [-45, -30],
+ k: [0, -30],
+ l: [45, -30],
+ m: [-45, 30],
+ n: [0, 30],
+ o: [45, 30],
+ p: [-45, 0],
+ A: [-81, -51],
+ B: [-27, -51],
+ C: [27, -51],
+ D: [78, -51],
+ K: [-81, -17],
+ E: [40.5, -17],
+ J: [-81, 17],
+ F: [40.5, 17],
+ I: [-81, 51],
+ G: [78, 51],
+ H: [-27, 51],
+ L: [27, 51]
+ },
+ banner: {
+ a: [-50, -50],
+ b: [0, -50],
+ c: [50, -50],
+ d: [-50, 0],
+ e: [0, 0],
+ f: [50, 0],
+ g: [-50, 40],
+ h: [0, 40],
+ i: [50, 40],
+ y: [-50, -50],
+ z: [0, 40],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 27.5],
+ n: [0, 27.5],
+ o: [37.5, 27.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.5, -66.5],
+ B: [-22, -66.5],
+ C: [22, -66.5],
+ D: [66.5, -66.5],
+ K: [-66.5, -20],
+ E: [66.5, -20],
+ J: [-66.5, 26],
+ F: [66.5, 26],
+ I: [-66.5, 66.5],
+ G: [66.5, 66.5],
+ H: [-25, 75],
+ L: [25, 75]
+ },
+ dovetail: {
+ a: [-49.75, -50],
+ b: [0, -50],
+ c: [49.75, -50],
+ d: [-49.75, 0],
+ e: [0, 0],
+ f: [49.75, 0],
+ g: [-49.75, 50],
+ i: [49.75, 50],
+ y: [-50, -50],
+ z: [0, 40],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 32.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.5, -66.5],
+ B: [-22, -66.5],
+ C: [22, -66.5],
+ D: [66.5, -66.5],
+ K: [-66.5, -16.5],
+ E: [66.5, -16.5],
+ J: [-66.5, 34.5],
+ F: [66.5, 34.5],
+ I: [-66.5, 84.5],
+ G: [66.5, 84.5],
+ H: [-25, 64],
+ L: [25, 64]
+ },
+ gonfalon: {
+ a: [-49.75, -50],
+ b: [0, -50],
+ c: [49.75, -50],
+ d: [-49.75, 0],
+ e: [0, 0],
+ f: [49.75, 0],
+ g: [-49.75, 50],
+ h: [0, 50],
+ i: [49.75, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.5, -66.5],
+ B: [-22, -66.5],
+ C: [22, -66.5],
+ D: [66.5, -66.5],
+ K: [-66.5, -20],
+ E: [66.5, -20],
+ J: [-66.5, 26],
+ F: [66.5, 26],
+ I: [-40, 63],
+ G: [40, 63],
+ H: [0, 88]
+ },
+ pennant: {
+ a: [-45, -50],
+ b: [0, -50],
+ c: [45, -50],
+ e: [0, 0],
+ h: [0, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-32.5, -37.5],
+ k: [0, -37.5],
+ l: [32.5, -37.5],
+ n: [0, 37.5],
+ A: [-60, -76],
+ B: [-22, -76],
+ C: [22, -76],
+ D: [60, -76],
+ K: [-46, -38],
+ E: [46, -38],
+ J: [-31, 0],
+ F: [31, 0],
+ I: [-16, 38],
+ G: [16, 38],
+ H: [0, 76]
+ },
+ round: {
+ a: [-40, -40],
+ b: [0, -40],
+ c: [40, -40],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-40, 40],
+ h: [0, 40],
+ i: [40, 40],
+ y: [-48, -48],
+ z: [0, 57.5],
+ j: [-35.5, -35.5],
+ k: [0, -37.5],
+ l: [35.5, -35.5],
+ m: [-35.5, 35.5],
+ n: [0, 37.5],
+ o: [35.5, 35.5],
+ p: [-36.5, 0],
+ q: [36.5, 0],
+ A: [-59, -48],
+ B: [-23, -73],
+ C: [23, -73],
+ D: [59, -48],
+ K: [-76, -10],
+ E: [76, -10],
+ J: [-70, 31],
+ F: [70, 31],
+ I: [-42, 64],
+ G: [42, 64],
+ H: [0, 77]
+ },
+ oval: {
+ a: [-37.5, -50],
+ b: [0, -50],
+ c: [37.5, -50],
+ d: [-43, 0],
+ e: [0, 0],
+ f: [43, 0],
+ g: [-37.5, 50],
+ h: [0, 50],
+ i: [37.5, 50],
+ y: [-48, -48],
+ z: [0, 60],
+ j: [-35.5, -37.5],
+ k: [0, -37.5],
+ l: [35.5, -37.5],
+ m: [-35.5, 37.5],
+ n: [0, 50],
+ o: [35.5, 37.5],
+ p: [-36.5, 0],
+ q: [36.5, 0],
+ A: [-48, -48],
+ B: [-23, -78],
+ C: [23, -78],
+ D: [48, -48],
+ K: [-59, -10],
+ E: [59, -10],
+ J: [-55, 31],
+ F: [55, 31],
+ I: [-36, 68],
+ G: [36, 68],
+ H: [0, 85]
+ },
+ vesicaPiscis: {
+ a: [-32, -37],
+ b: [0, -50],
+ c: [32, -37],
+ d: [-32, 0],
+ e: [0, 0],
+ f: [32, 0],
+ g: [-32, 37],
+ h: [0, 50],
+ i: [32, 37],
+ y: [-50, -50],
+ z: [0, 62],
+ j: [-27.5, -27.5],
+ k: [0, -37],
+ l: [27.5, -27.5],
+ m: [-27.5, 27.5],
+ n: [0, 42],
+ o: [27.5, 27.5],
+ p: [-27.5, 0],
+ q: [27.5, 0],
+ A: [-45, -32],
+ B: [-29, -63],
+ C: [29, -63],
+ D: [45, -32],
+ K: [-50, 0],
+ E: [50, 0],
+ J: [-45, 32],
+ F: [45, 32],
+ I: [-29, 63],
+ G: [29, 63],
+ H: [0, 89],
+ L: [0, -89]
+ },
+ square: {
+ a: [-49.75, -50],
+ b: [0, -50],
+ c: [49.75, -50],
+ d: [-49.75, 0],
+ e: [0, 0],
+ f: [49.75, 0],
+ g: [-49.75, 50],
+ h: [0, 50],
+ i: [49.75, 50],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-37.5, -37.5],
+ k: [0, -37.5],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 37.5],
+ o: [37.5, 37.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-66.5, -66.5],
+ B: [-22, -66.5],
+ C: [22, -66.5],
+ D: [66.5, -66.5],
+ K: [-66.5, -20],
+ E: [66.5, -20],
+ J: [-66.5, 26],
+ F: [66.5, 26],
+ I: [-66.5, 66.5],
+ G: [66.5, 66.5],
+ H: [-22, 66.5],
+ L: [22, 66.5]
+ },
+ diamond: {
+ a: [-32, -37],
+ b: [0, -50],
+ c: [32, -37],
+ d: [-43, 0],
+ e: [0, 0],
+ f: [43, 0],
+ g: [-32, 37],
+ h: [0, 50],
+ i: [32, 37],
+ y: [-50, -50],
+ z: [0, 62],
+ j: [-27.5, -27.5],
+ k: [0, -37],
+ l: [27.5, -27.5],
+ m: [-27.5, 27.5],
+ n: [0, 42],
+ o: [27.5, 27.5],
+ p: [-37, 0],
+ q: [37, 0],
+ A: [-43, -28],
+ B: [-22, -56],
+ C: [22, -56],
+ D: [43, -28],
+ K: [-63, 0],
+ E: [63, 0],
+ J: [-42, 28],
+ F: [42, 28],
+ I: [-22, 56],
+ G: [22, 56],
+ H: [0, 83],
+ L: [0, -82]
+ },
+ no: {
+ a: [-66.5, -66.5],
+ b: [0, -66.5],
+ c: [66.5, -66.5],
+ d: [-66.5, 0],
+ e: [0, 0],
+ f: [66.5, 0],
+ g: [-66.5, 66.5],
+ h: [0, 66.5],
+ i: [66.5, 66.5],
+ y: [-50, -50],
+ z: [0, 75],
+ j: [-50, -50],
+ k: [0, -50],
+ l: [50, -50],
+ m: [-50, 50],
+ n: [0, 50],
+ o: [50, 50],
+ p: [-50, 0],
+ q: [50, 0],
+ A: [-91.5, -91.5],
+ B: [-30.5, -91.5],
+ C: [30.5, -91.5],
+ D: [91.5, -91.5],
+ K: [-91.5, -30.5],
+ E: [91.5, -30.5],
+ J: [-91.5, 30.5],
+ F: [91.5, 30.5],
+ I: [-91.5, 91.5],
+ G: [91.5, 91.5],
+ H: [-30.5, 91.5],
+ L: [30.5, 91.5]
+ },
+ fantasy1: {
+ a: [-45, -45],
+ b: [0, -50],
+ c: [45, -45],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-36, 42.5],
+ h: [0, 50],
+ i: [36, 42.5],
+ y: [-50, -50],
+ z: [0, 60],
+ j: [-37, -37],
+ k: [0, -40],
+ l: [37, -37],
+ m: [-32, 32],
+ n: [0, 40],
+ o: [32, 32],
+ p: [-28.5, 0],
+ q: [28.5, 0],
+ A: [-66, -55],
+ B: [-22, -67],
+ C: [22, -67],
+ D: [66, -55],
+ K: [-53, -20],
+ E: [53, -20],
+ J: [-46, 26],
+ F: [46, 26],
+ I: [-29, 62],
+ G: [29, 62],
+ H: [0, 84]
+ },
+ fantasy2: {
+ a: [-45, -45],
+ b: [0, -45],
+ c: [45, -45],
+ d: [-35, 0],
+ e: [0, 0],
+ f: [35, 0],
+ g: [-36, 42.5],
+ h: [0, 45],
+ i: [36, 42.5],
+ y: [-50, -50],
+ z: [0, 55],
+ j: [-32.5, -32.5],
+ k: [0, -40],
+ l: [32.5, -32.5],
+ m: [-30, 30],
+ n: [0, 40],
+ o: [30, 30],
+ p: [-27.5, 0],
+ q: [27.5, 0],
+ A: [-58, -35],
+ B: [-44, -67],
+ C: [44, -67],
+ D: [58, -35],
+ K: [-39, -5],
+ E: [39, -5],
+ J: [-57, 26],
+ F: [57, 26],
+ I: [-32, 58],
+ G: [32, 58],
+ H: [0, 83],
+ L: [0, -72]
+ },
+ fantasy3: {
+ a: [-40, -45],
+ b: [0, -50],
+ c: [40, -45],
+ d: [-35, 0],
+ e: [0, 0],
+ f: [35, 0],
+ g: [-36, 42.5],
+ h: [0, 50],
+ i: [36, 42.5],
+ y: [-50, -50],
+ z: [0, 55],
+ j: [-32.5, -32.5],
+ k: [0, -40],
+ l: [32.5, -32.5],
+ m: [-30, 30],
+ n: [0, 40],
+ o: [30, 30],
+ p: [-27.5, 0],
+ q: [27.5, 0],
+ A: [-56, -42],
+ B: [-22, -72],
+ C: [22, -72],
+ D: [56, -42],
+ K: [-37, -11],
+ E: [37, -11],
+ J: [-60, 20],
+ F: [60, 20],
+ I: [-34, 56],
+ G: [34, 56],
+ H: [0, 83]
+ },
+ fantasy4: {
+ a: [-50, -45],
+ b: [0, -50],
+ c: [50, -45],
+ d: [-45, 0],
+ e: [0, 0],
+ f: [45, 0],
+ g: [-40, 45],
+ h: [0, 50],
+ i: [40, 45],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-37.5, -37.5],
+ k: [0, -45],
+ l: [37.5, -37.5],
+ m: [-37.5, 37.5],
+ n: [0, 45],
+ o: [37.5, 37.5],
+ p: [-35, 0],
+ q: [35, 0],
+ A: [-75, -56],
+ B: [-36, -61],
+ C: [36, -61],
+ D: [75, -56],
+ K: [-67, -12],
+ E: [67, -12],
+ J: [-63, 32],
+ F: [63, 32],
+ I: [-42, 75],
+ G: [42, 75],
+ H: [0, 91.5],
+ L: [0, -79]
+ },
+ fantasy5: {
+ a: [-45, -50],
+ b: [0, -50],
+ c: [45, -50],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-30, 45],
+ h: [0, 50],
+ i: [30, 45],
+ y: [-50, -50],
+ z: [0, 60],
+ j: [-37, -37],
+ k: [0, -40],
+ l: [37, -37],
+ m: [-32, 32],
+ n: [0, 40],
+ o: [32, 32],
+ p: [-28.5, 0],
+ q: [28.5, 0],
+ A: [-61, -67],
+ B: [-22, -76],
+ C: [22, -76],
+ D: [61, -67],
+ K: [-58, -25],
+ E: [58, -25],
+ J: [-48, 20],
+ F: [48, 20],
+ I: [-28.5, 60],
+ G: [28.5, 60],
+ H: [0, 89]
+ },
+ noldor: {
+ b: [0, -65],
+ e: [0, -15],
+ h: [0, 35],
+ z: [0, 35],
+ k: [0, -50],
+ n: [0, 30],
+ p: [-20, -15],
+ q: [20, -15],
+ A: [-34, -47],
+ B: [-20, -68],
+ C: [20, -68],
+ D: [34, -47],
+ K: [-18, -20],
+ E: [18, -20],
+ J: [-26, 11],
+ F: [26, 11],
+ I: [-14, 43],
+ G: [14, 43],
+ H: [0, 74],
+ L: [0, -85]
+ },
+ gondor: {
+ a: [-32.5, -50],
+ b: [0, -50],
+ c: [32.5, -50],
+ d: [-32.5, 0],
+ e: [0, 0],
+ f: [32.5, 0],
+ g: [-32.5, 50],
+ h: [0, 50],
+ i: [32.5, 50],
+ y: [-42.5, -52.5],
+ z: [0, 65],
+ j: [-25, -37.5],
+ k: [0, -37.5],
+ l: [25, -37.5],
+ m: [-25, 30],
+ n: [0, 37.5],
+ o: [25, 30],
+ p: [-25, 0],
+ q: [25, 0],
+ A: [-42, -52],
+ B: [-17, -75],
+ C: [17, -75],
+ D: [42, -52],
+ K: [-42, -15],
+ E: [42, -15],
+ J: [-42, 22],
+ F: [42, 22],
+ I: [-26, 60],
+ G: [26, 60],
+ H: [0, 87]
+ },
+ easterling: {
+ a: [-40, -47.5],
+ b: [0, -47.5],
+ c: [40, -47.5],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-40, 47.5],
+ h: [0, 47.5],
+ i: [40, 47.5],
+ y: [-42.5, -52.5],
+ z: [0, 65],
+ j: [-30, -37.5],
+ k: [0, -37.5],
+ l: [30, -37.5],
+ m: [-30, 37.5],
+ n: [0, 37.5],
+ o: [30, 37.5],
+ p: [-30, 0],
+ q: [30, 0],
+ A: [-52, -72],
+ B: [0, -65],
+ D: [52, -72],
+ K: [-52, -24],
+ E: [52, -24],
+ J: [-52, 24],
+ F: [52, 24],
+ I: [-52, 72],
+ G: [52, 72],
+ H: [0, 65]
+ },
+ erebor: {
+ a: [-40, -40],
+ b: [0, -55],
+ c: [40, -40],
+ d: [-40, 0],
+ e: [0, 0],
+ f: [40, 0],
+ g: [-40, 40],
+ h: [0, 55],
+ i: [40, 40],
+ y: [-50, -50],
+ z: [0, 50],
+ j: [-35, -35],
+ k: [0, -45],
+ l: [35, -35],
+ m: [-35, 35],
+ n: [0, 45],
+ o: [35, 35],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-47, -46],
+ B: [-22, -81],
+ C: [22, -81],
+ D: [47, -46],
+ K: [-66.5, 0],
+ E: [66.5, 0],
+ J: [-47, 46],
+ F: [47, 46],
+ I: [-22, 81],
+ G: [22, 81]
+ },
+ ironHills: {
+ a: [-43.75, -50],
+ b: [0, -50],
+ c: [43.75, -50],
+ d: [-43.25, 0],
+ e: [0, 0],
+ f: [43.25, 0],
+ g: [-42.5, 42.5],
+ h: [0, 50],
+ i: [42.5, 42.5],
+ y: [-50, -50],
+ z: [0, 62.5],
+ j: [-32.5, -32.5],
+ k: [0, -40],
+ l: [32.5, -32.5],
+ m: [-32.5, 32.5],
+ n: [0, 40],
+ o: [32.5, 32.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-61, -67],
+ B: [-22, -74],
+ C: [22, -74],
+ D: [61, -67],
+ K: [-59, -20],
+ E: [59, -20],
+ J: [-57, 26],
+ F: [57, 26],
+ I: [-33, 64],
+ G: [33, 64],
+ H: [0, 88]
+ },
+ urukHai: {
+ a: [-40, -45],
+ b: [0, -45],
+ c: [40, -45],
+ d: [-36, 0],
+ e: [0, 0],
+ f: [36, 0],
+ g: [-32.25, 40],
+ h: [0, 40],
+ i: [32.25, 40],
+ y: [-50, -50],
+ z: [0, 40],
+ j: [-32.5, -32.5],
+ k: [0, -37.5],
+ l: [32.5, -32.5],
+ m: [-27.5, 27.5],
+ n: [0, 32.5],
+ o: [27.5, 27.5],
+ p: [-37.5, 0],
+ q: [37.5, 0],
+ A: [-31, -79],
+ B: [-1, -90],
+ C: [31, -74],
+ D: [61, -57],
+ K: [-55, -19],
+ E: [53, -19],
+ J: [-45, 19],
+ F: [45, 19],
+ I: [-33, 57],
+ G: [35, 57],
+ H: [0, 57],
+ L: [-39, -50]
+ },
+ moriaOrc: {
+ a: [-37.5, -37.5],
+ b: [0, -37.5],
+ c: [37.5, -37.5],
+ d: [-37.5, 0],
+ e: [0, 0],
+ f: [37.5, 0],
+ g: [-37.5, 37.5],
+ h: [0, 37.5],
+ i: [37.5, 37.5],
+ y: [-50, -50],
+ z: [0, 40],
+ j: [-30, -30],
+ k: [0, -30],
+ l: [30, -30],
+ m: [-30, 30],
+ n: [0, 30],
+ o: [30, 30],
+ p: [-30, 0],
+ q: [30, 0],
+ A: [-48, -48],
+ B: [-16, -50],
+ C: [16, -46],
+ D: [39, -61],
+ K: [-52, -19],
+ E: [52, -26],
+ J: [-42, 9],
+ F: [52, 9],
+ I: [-31, 40],
+ G: [40, 43],
+ H: [4, 47]
+ }
+ };
+
+ const shieldSize = {
+ horsehead: 0.9,
+ horsehead2: 0.9,
+ polish: 0.85,
+ swiss: 0.95,
+ boeotian: 0.75,
+ roman: 0.95,
+ kite: 0.65,
+ targe2: 0.9,
+ pavise: 0.9,
+ wedged: 0.95,
+ flag: 0.7,
+ pennon: 0.5,
+ guidon: 0.65,
+ banner: 0.8,
+ dovetail: 0.8,
+ pennant: 0.6,
+ oval: 0.95,
+ vesicaPiscis: 0.8,
+ diamond: 0.8,
+ no: 1.2,
+ fantasy1: 0.8,
+ fantasy2: 0.7,
+ fantasy3: 0.7,
+ fantasy5: 0.9,
+ noldor: 0.5,
+ gondor: 0.75,
+ easterling: 0.8,
+ erebor: 0.9,
+ urukHai: 0.8,
+ moriaOrc: 0.7
+ };
+
+ const shieldBox = {
+ heater: "0 10 200 200",
+ spanish: "0 10 200 200",
+ french: "0 10 200 200",
+
+ horsehead: "0 10 200 200",
+ horsehead2: "0 10 200 200",
+ polish: "0 0 200 200",
+ hessen: "0 5 200 200",
+ swiss: "0 10 200 200",
+
+ boeotian: "0 0 200 200",
+ roman: "0 0 200 200",
+ kite: "0 0 200 200",
+ oldFrench: "0 10 200 200",
+ renaissance: "0 5 200 200",
+ baroque: "0 10 200 200",
+
+ targe: "0 0 200 200",
+ targe2: "0 0 200 200",
+ pavise: "0 0 200 200",
+ wedged: "0 10 200 200",
+
+ flag: "0 0 200 200",
+ pennon: "2.5 0 200 200",
+ guidon: "2.5 0 200 200",
+ banner: "0 10 200 200",
+ dovetail: "0 10 200 200",
+ gonfalon: "0 10 200 200",
+ pennant: "0 0 200 200",
+
+ round: "0 0 200 200",
+ oval: "0 0 200 200",
+ vesicaPiscis: "0 0 200 200",
+ square: "0 0 200 200",
+ diamond: "0 0 200 200",
+ no: "0 0 200 200",
+
+ fantasy1: "0 0 200 200",
+ fantasy2: "0 5 200 200",
+ fantasy3: "0 5 200 200",
+ fantasy4: "0 5 200 200",
+ fantasy5: "0 0 200 200",
+
+ noldor: "0 0 200 200",
+ gondor: "0 5 200 200",
+ easterling: "0 0 200 200",
+ erebor: "0 0 200 200",
+ ironHills: "0 5 200 200",
+ urukHai: "0 0 200 200",
+ moriaOrc: "0 0 200 200"
+ };
+
+ const shieldPaths = {
+ heater: "m25,25 h150 v50 a150,150,0,0,1,-75,125 a150,150,0,0,1,-75,-125 z",
+ spanish: "m25,25 h150 v100 a75,75,0,0,1,-150,0 z",
+ french: "m 25,25 h 150 v 139.15 c 0,41.745 -66,18.15 -75,36.3 -9,-18.15 -75,5.445 -75,-36.3 v 0 z",
+ horsehead:
+ "m 20,40 c 0,60 40,80 40,100 0,10 -4,15 -0.35,30 C 65,185.7 81,200 100,200 c 19.1,0 35.3,-14.6 40.5,-30.4 C 144.2,155 140,150 140,140 140,120 180,100 180,40 142.72,40 150,15 100,15 55,15 55,40 20,40 Z",
+ horsehead2:
+ "M60 20c-5 20-10 35-35 55 25 35 35 65 30 100 20 0 35 10 45 26 10-16 30-26 45-26-5-35 5-65 30-100a87 87 0 01-35-55c-25 3-55 3-80 0z",
+ polish:
+ "m 90.3,6.3 c -12.7,0 -20.7,10.9 -40.5,14 0,11.8 -4.9,23.5 -11.4,31.1 0,0 12.7,6 12.7,19.3 C 51.1,90.8 30,90.8 30,90.8 c 0,0 -3.6,7.4 -3.6,22.4 0,34.3 23.1,60.2 40.7,68.2 17.6,8 27.7,11.4 32.9,18.6 5.2,-7.3 15.3,-10.7 32.8,-18.6 17.6,-8 40.7,-33.9 40.7,-68.2 0,-15 -3.6,-22.4 -3.6,-22.4 0,0 -21.1,0 -21.1,-20.1 0,-13.3 12.7,-19.3 12.7,-19.3 C 155.1,43.7 150.2,32.1 150.2,20.3 130.4,17.2 122.5,6.3 109.7,6.3 102.5,6.3 100,10 100,10 c 0,0 -2.5,-3.7 -9.7,-3.7 z",
+ hessen:
+ "M170 20c4 5 8 13 15 20 0 0-10 0-10 15 0 100-15 140-75 145-65-5-75-45-75-145 0-15-10-15-10-15l15-20c0 15 10-5 70-5s70 20 70 5z",
+ swiss:
+ "m 25,20 c -0.1,0 25.2,8.5 37.6,8.5 C 75.1,28.5 99.1,20 100,20 c 0.6,0 24.9,8.5 37.3,8.5 C 149.8,28.5 174.4,20 175,20 l -0.3,22.6 C 173.2,160.3 100,200 100,200 100,200 26.5,160.9 25.2,42.6 Z",
+ boeotian:
+ "M150 115c-5 0-10-5-10-15s5-15 10-15c10 0 7 10 15 10 10 0 0-30 0-30-10-25-30-55-65-55S45 40 35 65c0 0-10 30 0 30 8 0 5-10 15-10 5 0 10 5 10 15s-5 15-10 15c-10 0-7-10-15-10-10 0 0 30 0 30 10 25 30 55 65 55s55-30 65-55c0 0 10-30 0-30-8 0-5 10-15 10z",
+ roman: "m 160,170 c -40,20 -80,20 -120,0 V 30 C 80,10 120,10 160,30 Z",
+ kite: "m 53.3,46.4 c 0,4.1 1,12.3 1,12.3 7.1,55.7 45.7,141.3 45.7,141.3 0,0 38.6,-85.6 45.7,-141.2 0,0 1,-8.1 1,-12.3 C 146.7,20.9 125.8,0.1 100,0.1 74.2,0.1 53.3,20.9 53.3,46.4 Z",
+ oldFrench: "m25,25 h150 v75 a100,100,0,0,1,-75,100 a100,100,0,0,1,-75,-100 z",
+ renaissance:
+ "M 25,33.9 C 33.4,50.3 36.2,72.9 36.2,81.7 36.2,109.9 25,122.6 25,141 c 0,29.4 24.9,44.1 40.2,47.7 15.3,3.7 29.3,0 34.8,11.3 5.5,-11.3 19.6,-7.6 34.8,-11.3 C 150.1,185 175,170.3 175,141 c 0,-18.4 -11.2,-31.1 -11.2,-59.3 0,-8.8 2.8,-31.3 11.2,-47.7 L 155.7,14.4 C 138.2,21.8 119.3,25.7 100,25.7 c -19.3,0 -38.2,-3.9 -55.7,-11.3 z",
+ baroque:
+ "m 100,25 c 18,0 50,2 75,14 v 37 l -2.7,3.2 c -4.9,5.4 -6.6,9.6 -6.7,16.2 0,6.5 2,11.6 6.9,17.2 l 2.8,3.1 v 10.2 c 0,17.7 -2.2,27.7 -7.8,35.9 -5,7.3 -11.7,11.3 -32.3,19.4 -12.6,5 -20.2,8.8 -28.6,14.5 C 103.3,198 100,200 100,200 c 0,0 -2.8,-2.3 -6.4,-4.7 C 85.6,189.8 78,186 65,180.9 32.4,168.1 26.9,160.9 25.8,129.3 L 25,116 l 3.3,-3.3 c 4.8,-5.2 7,-10.7 7,-17.3 0,-6.8 -1.8,-11.1 -6.5,-16.1 L 25,76 V 39 C 50,27 82,25 100,25 Z",
+ targe:
+ "m 20,35 c 15,0 115,-60 155,-10 -5,10 -15,15 -10,50 5,45 10,70 -10,90 C 125,195 75,195 50,175 25,150 30,130 35,85 50,95 65,85 65,70 65,50 50,45 40,50 30,55 27,65 30,70 23,73 20,70 14,70 11,60 20,45 20,35 Z",
+ targe2:
+ "m 84,32.2 c 6.2,-1 19.5,-31.4 94.1,-20.2 -30.57,33.64 -21.66,67.37 -11.2,95 20.2,69.5 -41.17549,84.7 -66.88,84.7 C 74.32,191.7071 8.38,168.95 32,105.9 36.88,92.88 31,89 31,82.6 35.15,82.262199 56.79,86.17 56.5,69.8 56.20,52.74 42.2,47.9 25.9,55.2 25.9,51.4 39.8,6.7 84,32.2 Z",
+ pavise:
+ "M95 7L39.9 37.3a10 10 0 00-5.1 9.5L46 180c.4 5.2 3.7 10 9 10h90c5.3 0 9.6-4.8 10-10l10.6-133.2a10 10 0 00-5-9.5L105 7c-4.2-2.3-6.2-2.3-10 0z",
+ wedged:
+ "m 51.2,19 h 96.4 c 3.1,12.7 10.7,20.9 26.5,20.8 C 175.7,94.5 165.3,144.3 100,200 43.5,154.2 22.8,102.8 25.1,39.7 37,38.9 47.1,34.7 51.2,19 Z",
+ round: "m 185,100 a 85,85 0 0 1 -85,85 85,85 0 0 1 -85,-85 85,85 0 0 1 85,-85 85,85 0 0 1 85,85",
+ oval: "m 32.3,99.5 a 67.7,93.7 0 1 1 0,1.3 z",
+ vesicaPiscis:
+ "M 100,0 C 63.9,20.4 41,58.5 41,100 c 0,41.5 22.9,79.6 59,100 36.1,-20.4 59,-58.5 59,-100 C 159,58.5 136.1,20.4 100,0 Z",
+ square: "M 25,25 H 175 V 175 H 25 Z",
+ diamond: "M 25,100 100,200 175,100 100,0 Z",
+ no: "m0,0 h200 v200 h-200 z",
+ flag: "M 10,40 h180 v120 h-180 Z",
+ pennon: "M 10,40 l190,60 -190,60 Z",
+ guidon: "M 10,40 h190 l-65,60 65,60 h-190 Z",
+ banner: "m 25,25 v 170 l 25,-40 25,40 25,-40 25,40 25,-40 25,40 V 25 Z",
+ dovetail: "m 25,25 v 175 l 75,-40 75,40 V 25 Z",
+ gonfalon: "m 25,25 v 125 l 75,50 75,-50 V 25 Z",
+ pennant: "M 25,15 100,200 175,15 Z",
+ fantasy1:
+ "M 100,5 C 85,30 40,35 15,40 c 40,35 20,90 40,115 15,25 40,30 45,45 5,-15 30,-20 45,-45 20,-25 0,-80 40,-115 C 160,35 115,30 100,5 Z",
+ fantasy2:
+ "m 152,21 c 0,0 -27,14 -52,-4 C 75,35 48,21 48,21 50,45 30,55 30,75 60,75 60,115 32,120 c 3,40 53,50 68,80 15,-30 65,-40 68,-80 -28,-5 -28,-45 2,-45 C 170,55 150,45 152,21 Z",
+ fantasy3:
+ "M 167,67 C 165,0 35,0 33,67 c 32,-7 27,53 -3,43 -5,45 60,65 70,90 10,-25 75,-47.51058 70,-90 -30,10 -35,-50 -3,-43 z",
+ fantasy4:
+ "M100 9C55 48 27 27 13 39c23 50 3 119 49 150 14 9 28 11 38 11s27-4 38-11c55-39 24-108 49-150-14-12-45 7-87-30z",
+ fantasy5: "M 100,0 C 75,25 30,25 30,25 c 0,69 20,145 70,175 50,-30 71,-106 70,-175 0,0 -45,0 -70,-25 z",
+ noldor:
+ "m 55,75 h 2 c 3,-25 38,-10 3,20 15,50 30,75 40,105 10,-30 25,-55 40,-105 -35,-30 0,-45 3,-20 h 2 C 150,30 110,20 100,0 90,20 50,30 55,75 Z",
+ gondor: "m 100,200 c 15,-15 38,-35 45,-60 h 5 V 30 h -5 C 133,10 67,10 55,30 h -5 v 110 h 5 c 7,25 30,45 45,60 z",
+ easterling: "M 160,185 C 120,170 80,170 40,185 V 15 c 40,15 80,15 120,0 z",
+ erebor: "M25 135 V60 l22-13 16-37 h75 l15 37 22 13 v75l-22 18-16 37 H63l-16-37z",
+ ironHills: "m 30,25 60,-10 10,10 10,-10 60,10 -5,125 -65,50 -65,-50 z",
+ urukHai: "M 30,60 C 40,60 60,50 60,20 l -5,-3 45,-17 75,40 -5,5 -35,155 -5,-35 H 70 v 35 z",
+ moriaOrc:
+ "M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z"
+ };
+
+ const lines = {
+ straight: "m 0,100 v15 h 200 v -15 z",
+ engrailed:
+ "m 0,95 a 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 v 20 H 0 Z",
+ invecked:
+ "M0,102.5 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 v12.5 H0 z",
+ embattled:
+ "M 0,105 H 2.5 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 2.5 v 10 H 0 Z",
+ wavy: "m 200,115 v -15 c -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 v 15 z",
+ raguly:
+ "m 200,95 h -3 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 H 97 l -5,10 H 82 L 87,95 H 77 l -5,10 H 62 L 67,95 H 57 l -5,10 H 42 L 47,95 H 37 l -5,10 H 22 L 27,95 H 17 l -5,10 H 2 L 7,95 H 0 v 20 h 200 z",
+ dancetty:
+ "m 0,105 10,-15 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 10,15 v 10 H 0 Z",
+ dentilly:
+ "M 180,105 170,95 v 10 L 160,95 v 10 L 150,95 v 10 L 140,95 v 10 L 130,95 v 10 L 120,95 v 10 L 110,95 v 10 L 100,95 v 10 L 90,95 v 10 L 80,95 v 10 L 70,95 v 10 L 60,95 v 10 L 50,95 v 10 L 40,95 v 10 L 30,95 v 10 L 20,95 v 10 L 10,95 v 10 L 0,95 v 20 H 200 V 105 L 190,95 v 10 L 180,95 Z",
+ angled: "m 0,95 h 100 v 10 h 100 v 10 H 0 Z",
+ urdy: "m 200,90 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 L 0,90 v 25 h 200",
+ indented:
+ "m 100,95 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 v 20 H 0 V 95 l 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 z",
+ bevilled: "m 0,92.5 h 110 l -20,15 H 200 V 115 H 0 Z",
+ nowy: "m 0,95 h 80 c 0,0 0.1,20.1 20,20 19.9,-0.1 20,-20 20,-20 h 80 v 20 H 0 Z",
+ nowyReversed: "m 200,105 h -80 c 0,0 -0.1,-20.1 -20,-20 -19.9,0.1 -20,20 -20,20 H 0 v 10 h 200 z",
+ potenty:
+ "m 3,95 v 5 h 5 v 5 H 0 v 10 h 200 l 0.5,-10 H 193 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 H 100.5 93 v -5 h 5 V 95 H 83 v 5 h 5 v 5 H 73 v -5 h 5 V 95 H 63 v 5 h 5 v 5 H 53 v -5 h 5 V 95 H 43 v 5 h 5 v 5 H 33 v -5 h 5 V 95 H 23 v 5 h 5 v 5 H 13 v -5 h 5 v -5 z",
+ potentyDexter:
+ "m 200,105 h -2 v -10 0 0 h -10 v 5 h 5 v 5 H 183 V 95 h -10 v 5 h 5 v 5 H 168 V 95 h -10 v 5 h 5 v 5 H 153 V 95 h -10 v 5 h 5 v 5 H 138 V 95 h -10 v 5 h 5 v 5 H 123 V 95 h -10 v 5 h 5 v 5 h -10 v 0 0 -10 H 98 v 5 h 5 v 5 H 93 V 95 H 83 v 5 h 5 v 5 H 78 V 95 H 68 v 5 h 5 v 5 H 63 V 95 H 53 v 5 h 5 v 5 H 48 V 95 H 38 v 5 h 5 v 5 H 33 V 95 H 23 v 5 h 5 v 5 H 18 V 95 H 8 v 5 h 5 v 5 H 3 V 95 H 0 v 20 h 200 z",
+ potentySinister:
+ "m 2.5,95 v 10 H 0 v 10 h 202.5 v -15 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 z",
+ embattledGhibellin:
+ "M 200,200 V 100 l -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 v 15 h 200",
+ embattledNotched:
+ "m 200,105 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 H 90 V 95 l -5,5 -5,-5 v 10 H 75 V 95 l -5,5 -5,-5 v 10 H 60 V 95 l -5,5 -5,-5 v 10 H 45 V 95 l -5,5 -5,-5 v 10 H 30 V 95 l -5,5 -5,-5 v 10 H 15 V 95 l -5,5 -5,-5 v 10 H 0 v 10 h 200",
+ embattledGrady:
+ "m 0,95 v 20 H 200 V 95 h -2.5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 z",
+ dovetailed:
+ "m 200,95 h -7 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 H 93 l 4,10 H 83 L 87,95 H 73 l 4,10 H 63 L 67,95 H 53 l 4,10 H 43 L 47,95 H 33 l 4,10 H 23 L 27,95 H 13 l 4,10 H 3 L 7,95 H 0 v 20 h 200",
+ dovetailedIndented:
+ "m 200,100 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 v 15 h 200",
+ nebuly:
+ "m 13.1,89.8 c -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.2,4.5 -7.3,4.5 -0.5,0 -2.2,-0.2 -2.2,-0.2 V 115 h 200 v -10.1 c -3.7,-0.2 -6.7,-2.2 -6.7,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 -1.5,4.1 -4.2,4.4 -8.8,4.5 -4.7,-0.1 -8.7,-1.5 -8.9,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 z",
+ rayonne:
+ "M0 115l-.1-6 .2.8c1.3-1 2.3-2.5 2.9-4.4.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4A9 9 0 015.5 90c-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 2.1 3.1 3.1 4.6 1 1.6 2.4 3.1 2.7 4.8.3 1.7.3 3.3 0 5.2 1.3-1 2.6-2.7 3.2-4.6.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.75 2.79 2.72 4.08 4.45 5.82L200 115z",
+ seaWaves:
+ "m 28.83,94.9 c -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.33,-2.03 -2.19,-3.56 -4.45,-3.56 -4.24,0 -6.91,3.13 -8.5,5.13 V 115 h 200 v -14.89 c -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -6.6,3.09 -8.19,5.09 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 z",
+ dragonTeeth:
+ "M 9.4,85 C 6.5,88.1 4.1,92.9 3,98.8 1.9,104.6 2.3,110.4 3.8,115 2.4,113.5 0,106.6 0,109.3 v 5.7 h 200 v -5.7 c -1.1,-2.4 -2,-5.1 -2.6,-8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -1.4,-1.5 -2.8,-3.9 -3.8,-6.1 -1.1,-2.4 -2.3,-6.1 -2.6,-7.7 -0.2,-5.9 0.2,-11.7 1.7,-16.3 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 C 63,95.4 63.4,89.6 64.9,85 c -2.9,3.1 -5.3,7.9 -6.3,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.6,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 C 18.6,95.4 19,89.6 20.5,85 17.6,88.1 15.2,92.9 14.1,98.8 13,104.6 13.4,110.4 14.9,115 12,111.9 9.6,107.1 8.6,101.2 7.5,95.4 7.9,89.6 9.4,85 Z",
+ firTrees:
+ "m 3.9,90 -4,7 2,-0.5 L 0,100 v 15 h 200 v -15 l -1.9,-3.5 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 z",
+ flechy: "m 0,100 h 85 l 15,-15 15,15 h 85 v 15 H 0 Z",
+ barby: "m 0,100 h 85 l 15,15 15,-15 h 85 v 15 H 0 Z",
+ enclavy: "M 0,100 H 85 V 85 h 30 v 15 h 85 v 15 H 0 Z",
+ escartely: "m 0,100 h 85 v 15 h 30 v -15 h 85 v 15 H 0 Z",
+ arched: "m 100,95 c 40,-0.2 100,20 100,20 H 0 c 0,0 60,-19.8 100,-20 z",
+ archedReversed: "m 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z"
+ };
+
+ const templates = {
+ // straight divisions
+ perFess: ` `,
+ perPale: ` `,
+ perBend: ` `,
+ perBendSinister: ` `,
+ perChevron: ` `,
+ perChevronReversed: ` `,
+ perCross: ` `,
+ perPile: ` `,
+ perSaltire: ` `,
+ gyronny: ` `,
+ chevronny: ` `,
+ // lined divisions
+ perFessLined: line =>
+ ` `,
+ perPaleLined: line =>
+ ` `,
+ perBendLined: line =>
+ ` `,
+ perBendSinisterLined: line =>
+ ` `,
+ perChevronLined: line =>
+ ` `,
+ perChevronReversedLined: line =>
+ ` `,
+ perCrossLined: line =>
+ ` `,
+ perPileLined: line =>
+ ` `,
+ // straight ordinaries
+ fess: ` `,
+ pale: ` `,
+ bend: ` `,
+ bendSinister: ` `,
+ chief: ` `,
+ bar: ` `,
+ gemelle: ` `,
+ fessCotissed: ` `,
+ fessDoubleCotissed: ` `,
+ bendlet: ` `,
+ bendletSinister: ` `,
+ terrace: ` `,
+ cross: ` `,
+ crossParted: ` `,
+ saltire: ` `,
+ saltireParted: ` `,
+ mount: ` `,
+ point: ` `,
+ flaunches: ` `,
+ gore: ` `,
+ pall: ` `,
+ pallReversed: ` `,
+ chevron: ` `,
+ chevronReversed: ` `,
+ gyron: ` `,
+ quarter: ` `,
+ canton: ` `,
+ pile: ` `,
+ pileInBend: ` `,
+ pileInBendSinister: ` `,
+ piles: ` `,
+ pilesInPoint: ` `,
+ label: ` `,
+ // lined ordinaries
+ fessLined: line =>
+ ` `,
+ paleLined: line =>
+ ` `,
+ bendLined: line =>
+ ` `,
+ bendSinisterLined: line =>
+ ` `,
+ chiefLined: line =>
+ ` `,
+ barLined: line =>
+ ` `,
+ gemelleLined: line =>
+ ` `,
+ fessCotissedLined: line =>
+ ` `,
+ fessDoubleCotissedLined: line =>
+ ` `,
+ bendletLined: line =>
+ ` `,
+ bendletSinisterLined: line =>
+ ` `,
+ terraceLined: line =>
+ ` `,
+ crossLined: line =>
+ ` `,
+ crossPartedLined: line =>
+ ` `,
+ saltireLined: line =>
+ ` `,
+ saltirePartedLined: line =>
+ ` `
+ };
+
+ const patterns = {
+ semy: (p, c1, c2, size, chargeId) =>
+ ` `,
+ vair: (p, c1, c2, size) =>
+ ` `,
+ counterVair: (p, c1, c2, size) =>
+ ` `,
+ vairInPale: (p, c1, c2, size) =>
+ ` `,
+ vairEnPointe: (p, c1, c2, size) =>
+ ` `,
+ vairAncien: (p, c1, c2, size) =>
+ ` `,
+ potent: (p, c1, c2, size) =>
+ ` `,
+ counterPotent: (p, c1, c2, size) =>
+ ` `,
+ potentInPale: (p, c1, c2, size) =>
+ ` `,
+ potentEnPointe: (p, c1, c2, size) =>
+ ` `,
+ ermine: (p, c1, c2, size) =>
+ ` `,
+ chequy: (p, c1, c2, size) =>
+ ` `,
+ lozengy: (p, c1, c2, size) =>
+ ` `,
+ fusily: (p, c1, c2, size) =>
+ ` `,
+ pally: (p, c1, c2, size) =>
+ ` `,
+ barry: (p, c1, c2, size) =>
+ ` `,
+ gemelles: (p, c1, c2, size) =>
+ ` `,
+ bendy: (p, c1, c2, size) =>
+ ` `,
+ bendySinister: (p, c1, c2, size) =>
+ ` `,
+ palyBendy: (p, c1, c2, size) =>
+ ` `,
+ barryBendy: (p, c1, c2, size) =>
+ ` `,
+ pappellony: (p, c1, c2, size) =>
+ ` `,
+ pappellony2: (p, c1, c2, size) =>
+ ` `,
+ scaly: (p, c1, c2, size) =>
+ ` `,
+ plumetty: (p, c1, c2, size) =>
+ ` `,
+ masoned: (p, c1, c2, size) =>
+ ` `,
+ fretty: (p, c1, c2, size) =>
+ ` `,
+ grillage: (p, c1, c2, size) =>
+ ` `,
+ chainy: (p, c1, c2, size) =>
+ ` `,
+ maily: (p, c1, c2, size) =>
+ ` `,
+ honeycombed: (p, c1, c2, size) =>
+ ` `
+ };
+
+ const draw = async function (id, coa) {
+ const {shield = "heater", division, ordinaries = [], charges = []} = coa;
+
+ const ordinariesRegular = ordinaries.filter(o => !o.above);
+ const ordinariesAboveCharges = ordinaries.filter(o => o.above);
+ const shieldPath = shieldPaths[shield] || shieldPaths.heater;
+ const tDiv = division ? (division.t.includes("-") ? division.t.split("-")[1] : division.t) : null;
+ const positions = shieldPositions[shield];
+ const sizeModifier = shieldSize[shield] || 1;
+ const viewBox = shieldBox[shield] || "0 0 200 200";
+
+ const shieldClip = ` `;
+ const divisionClip = division
+ ? `${getTemplate(division.division, division.line)} `
+ : "";
+ const loadedCharges = await getCharges(coa, id, shieldPath);
+ const loadedPatterns = getPatterns(coa, id);
+ const blacklight = ` `;
+ const field = ` `;
+ const style = ``;
+
+ const divisionGroup = division ? templateDivision() : "";
+ const overlay = ` `;
+
+ const svg = `
+ ${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight}${style}
+ ${field}${divisionGroup}${templateAboveAll()}
+ ${overlay} `;
+
+ // insert coa svg to defs
+ document.getElementById("coas").insertAdjacentHTML("beforeend", svg);
+ return true;
+
+ function templateDivision() {
+ let svg = "";
+
+ // In field part
+ for (const ordinary of ordinariesRegular) {
+ if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
+ }
+
+ for (const charge of charges) {
+ if (charge.divided === "field") svg += templateCharge(charge, charge.t);
+ else if (charge.divided === "counter") svg += templateCharge(charge, tDiv);
+ }
+
+ for (const ordinary of ordinariesAboveCharges) {
+ if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
+ }
+
+ // In division part
+ svg += ` `;
+
+ for (const ordinary of ordinariesRegular) {
+ if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
+ }
+
+ for (const charge of charges) {
+ if (charge.divided === "division") svg += templateCharge(charge, charge.t);
+ else if (charge.divided === "counter") svg += templateCharge(charge, coa.t1);
+ }
+
+ for (const ordinary of ordinariesAboveCharges) {
+ if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
+ else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
+ }
+
+ return (svg += ` `);
+ }
+
+ function templateAboveAll() {
+ let svg = "";
+
+ ordinariesRegular
+ .filter(o => !o.divided)
+ .forEach(ordinary => {
+ svg += templateOrdinary(ordinary, ordinary.t);
+ });
+
+ charges
+ .filter(o => !o.divided || !division)
+ .forEach(charge => {
+ svg += templateCharge(charge, charge.t);
+ });
+
+ ordinariesAboveCharges
+ .filter(o => !o.divided)
+ .forEach(ordinary => {
+ svg += templateOrdinary(ordinary, ordinary.t);
+ });
+
+ return svg;
+ }
+
+ function templateOrdinary(ordinary, tincture) {
+ const fill = clr(tincture);
+ let svg = ``;
+ if (ordinary.ordinary === "bordure")
+ svg += ` `;
+ else if (ordinary.ordinary === "orle")
+ svg += ``;
+ else svg += getTemplate(ordinary.ordinary, ordinary.line);
+ return svg + ` `;
+ }
+
+ function templateCharge(charge, tincture, secondaryTincture, tertiaryTincture) {
+ const primary = clr(tincture);
+ const secondary = clr(secondaryTincture || tincture);
+ const tertiary = clr(tertiaryTincture || tincture);
+ const stroke = charge.stroke || "#000";
+
+ const chargePositions = [...new Set(charge.p)].filter(position => positions[position]);
+
+ let svg = ``;
+ for (const p of chargePositions) {
+ const transform = getElTransform(charge, p);
+ svg += ` `;
+ }
+ return svg + " ";
+
+ function getElTransform(c, p) {
+ const s = (c.size || 1) * sizeModifier;
+ const sx = c.sinister ? -s : s;
+ const sy = c.reversed ? -s : s;
+ let [x, y] = positions[p];
+ x = x - 100 * (sx - 1);
+ y = y - 100 * (sy - 1);
+ const scale = c.sinister || c.reversed ? `${sx} ${sy}` : s;
+ return `translate(${x} ${y}) scale(${scale})`;
+ }
+ }
+ };
+
+ async function getCharges(coa, id, shieldPath) {
+ let charges = coa.charges ? coa.charges.map(charge => charge.charge) : []; // add charges
+ if (semy(coa.t1)) charges.push(semy(coa.t1)); // add field semy charge
+ if (semy(coa.division?.t)) charges.push(semy(coa.division.t)); // add division semy charge
+
+ const uniqueCharges = [...new Set(charges)];
+ const fetchedCharges = await Promise.all(
+ uniqueCharges.map(async charge => {
+ if (charge === "inescutcheon")
+ return ` `;
+ const fetched = await fetchCharge(charge, id);
+ return fetched;
+ })
+ );
+ return fetchedCharges.join("");
+ }
+
+ const PATH = "./charges/";
+ async function fetchCharge(charge, id) {
+ const fetched = fetch(PATH + charge + ".svg")
+ .then(res => {
+ if (res.ok) return res.text();
+ else throw new Error("Cannot fetch charge");
+ })
+ .then(text => {
+ const html = document.createElement("html");
+ html.innerHTML = text;
+ const g = html.querySelector("g");
+ g.setAttribute("id", charge + "_" + id);
+ return g.outerHTML;
+ })
+ .catch(err => {
+ ERROR && console.error(err);
+ });
+ return fetched;
+ }
+
+ function getPatterns(coa, id) {
+ const isPattern = string => string.includes("-");
+ let patternsToAdd = [];
+ if (coa.t1.includes("-")) patternsToAdd.push(coa.t1); // add field pattern
+ if (coa.division && isPattern(coa.division.t)) patternsToAdd.push(coa.division.t); // add division pattern
+ if (coa.ordinaries)
+ coa.ordinaries.filter(ordinary => isPattern(ordinary.t)).forEach(ordinary => patternsToAdd.push(ordinary.t)); // add ordinaries pattern
+ if (coa.charges) coa.charges.filter(charge => isPattern(charge.t)).forEach(charge => patternsToAdd.push(charge.t)); // add charges pattern
+ if (!patternsToAdd.length) return "";
+
+ return [...new Set(patternsToAdd)]
+ .map(patternString => {
+ const [pattern, t1, t2, size] = patternString.split("-");
+ const charge = semy(patternString);
+ if (charge) return patterns.semy(patternString, clr(t1), clr(t2), getSizeMod(size), charge + "_" + id);
+ return patterns[pattern](patternString, clr(t1), clr(t2), getSizeMod(size), charge);
+ })
+ .join("");
+ }
+
+ function getSizeMod(size) {
+ if (size === "small") return 0.8;
+ if (size === "smaller") return 0.5;
+ if (size === "smallest") return 0.25;
+ if (size === "big") return 1.6;
+ return 1;
+ }
+
+ function getTemplate(id, line) {
+ const linedId = id + "Lined";
+ if (!line || line === "straight" || !templates[linedId]) return templates[id];
+ const linePath = lines[line];
+ return templates[linedId](linePath);
+ }
+
+ // get color or link to pattern
+ function clr(tincture) {
+ if (colors[tincture]) return colors[tincture];
+ return `url(#${tincture})`;
+ }
+
+ // get charge is string starts with "semy"
+ function semy(string) {
+ const isSemy = /^semy/.test(string);
+ if (!isSemy) return false;
+ return string.match(/semy_of_(.*?)-/)[1];
+ }
+
+ // render coa if does not exist
+ const trigger = async function (id, coa) {
+ if (!coa) return console.warn(`Emblem ${id} is undefined`);
+ if (coa.custom) return console.warn("Cannot render custom emblem", coa);
+ if (!document.getElementById(id)) return draw(id, coa);
+ };
+
+ const add = function (type, i, coa, x, y) {
+ const id = type + "COA" + i;
+ const g = document.getElementById(type + "Emblems");
+
+ if (emblems.selectAll("use").size()) {
+ const size = +g.getAttribute("font-size") || 50;
+ const use = ` `;
+ g.insertAdjacentHTML("beforeend", use);
+ }
+ if (layerIsOn("toggleEmblems")) trigger(id, coa);
+ };
+
+ return {trigger, add, shieldPaths};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./coa-renderer.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./coa-renderer_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in coa-renderer_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into coa-renderer_render.md
diff --git a/procedural/src/engine/support/coa-renderer_render.md b/procedural/src/engine/support/coa-renderer_render.md
new file mode 100644
index 00000000..8455c772
--- /dev/null
+++ b/procedural/src/engine/support/coa-renderer_render.md
@@ -0,0 +1,74 @@
+# coa-renderer_render.md
+
+Removed Rendering/UI Logic from coa-renderer.js
+
+The following code blocks, responsible for direct DOM manipulation, I/O, and UI-layer logic, were removed from coa-renderer.js. This logic must now be handled by the Viewer application.
+
+1. Direct SVG Injection into the DOM
+Original Code (in draw function):
+
+```javascript
+// insert coa svg to defs
+document.getElementById("coas").insertAdjacentHTML("beforeend", svg);
+return true;
+```
+Reason for Removal: Direct DOM manipulation. The engine must not know about or interact with the DOM. The refactored render function now returns the complete SVG string.
+
+2. File Fetching and Parsing (I/O)
+Original Code:
+
+```javascript
+async function fetchCharge(charge, id) {
+ const fetched = fetch(PATH + charge + ".svg")
+ .then(res => {
+ if (res.ok) return res.text();
+ else throw new Error("Cannot fetch charge");
+ })
+ .then(text => {
+ const html = document.createElement("html");
+ html.innerHTML = text;
+ const g = html.querySelector("g");
+ g.setAttribute("id", charge + "_" + id);
+ return g.outerHTML;
+ })
+ .catch(err => {
+ ERROR && console.error(err);
+ });
+ return fetched;
+}
+```
+
+Reason for Removal: Contains environment-specific I/O (fetch) and DOM parsing (document.createElement, innerHTML, querySelector). This entire responsibility is now shifted to the Viewer, which must provide the charge data to the engine.
+
+3. UI Triggering Logic
+Original Code:
+
+```javascript
+const trigger = async function (id, coa) {
+ if (!coa) return console.warn(`Emblem ${id} is undefined`);
+ if (coa.custom) return console.warn("Cannot render custom emblem", coa);
+ if (!document.getElementById(id)) return draw(id, coa);
+};
+```
+
+Reason for Removal: Checks for the existence of an element in the DOM (document.getElementById) to decide whether to render. This is UI-level conditional logic.
+
+4. Emblem Placement on Map
+Original Code:
+
+```javascript
+const add = function (type, i, coa, x, y) {
+ const id = type + "COA" + i;
+ const g = document.getElementById(type + "Emblems");
+
+ if (emblems.selectAll("use").size()) {
+ const size = +g.getAttribute("font-size") || 50;
+ const use = ` `;
+ g.insertAdjacentHTML("beforeend", use);
+ }
+ if (layerIsOn("toggleEmblems")) trigger(id, coa);
+};
+```
+
+Reason for Removal: This function is entirely for rendering/UI. It finds a specific SVG group on the map (#burgEmblems, #stateEmblems), reads its attributes, creates a element referencing the generated CoA, and inserts it. It also depends on another UI function (layerIsOn). This is quintessential Viewer logic.
+
diff --git a/procedural/src/engine/support/cultures-generator-original.txt b/procedural/src/engine/support/cultures-generator-original.txt
new file mode 100644
index 00000000..34dc5edd
--- /dev/null
+++ b/procedural/src/engine/support/cultures-generator-original.txt
@@ -0,0 +1,618 @@
+"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.
+ No cultures, states and burgs will be created.
+ 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.
+ Only ${count} out of ${culturesInput.value} requested cultures will be generated.
+ 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};
+})();
diff --git a/procedural/src/engine/support/cultures-generator_config.md b/procedural/src/engine/support/cultures-generator_config.md
new file mode 100644
index 00000000..68da5d7b
--- /dev/null
+++ b/procedural/src/engine/support/cultures-generator_config.md
@@ -0,0 +1,68 @@
+# Config Properties for cultures-generator.js
+
+The refactored `cultures-generator.js` module requires the following configuration properties to be passed via the `config` object parameter:
+
+## Required Configuration Properties
+
+### Culture Generation Settings
+- **`culturesInput`** (number) - Number of cultures requested by user
+ - *Replaces:* `+byId("culturesInput").value`
+ - *Usage:* Determines how many cultures to generate
+
+- **`culturesInSetNumber`** (number) - Maximum cultures available in selected culture set
+ - *Replaces:* `+byId("culturesSet").selectedOptions[0].dataset.max`
+ - *Usage:* Limits the number of cultures that can be generated based on the selected culture set
+
+- **`culturesSet`** (string) - Selected culture set type
+ - *Replaces:* `byId("culturesSet").value`
+ - *Usage:* Determines which predefined culture set to use
+ - *Valid values:* `"european"`, `"oriental"`, `"english"`, `"antique"`, `"highFantasy"`, `"darkFantasy"`, `"random"`, or default (all-world)
+
+### Map Dimensions
+- **`graphWidth`** (number) - Width of the map/graph
+ - *Usage:* Used for calculating culture center spacing
+ - *Note:* This should be derived from the map generation settings
+
+- **`graphHeight`** (number) - Height of the map/graph
+ - *Usage:* Used for calculating culture center spacing
+ - *Note:* This should be derived from the map generation settings
+
+### Culture Expansion Settings
+- **`sizeVariety`** (number) - Variety factor for culture expansionism
+ - *Replaces:* `byId("sizeVariety").value`
+ - *Usage:* Controls how much culture expansionism varies from base values
+ - *Typical range:* 0-2, where 1 is default variety
+
+- **`neutralRate`** (number, optional) - Rate modifier for culture expansion
+ - *Replaces:* `byId("neutralRate")?.valueAsNumber || 1`
+ - *Usage:* Affects maximum expansion cost calculations
+ - *Default:* 1 if not provided
+
+### Visual Settings
+- **`emblemShape`** (string) - Shield/emblem shape setting
+ - *Replaces:* `document.getElementById("emblemShape").value`
+ - *Usage:* Determines shield shapes for cultures
+ - *Valid values:* `"random"` for random shield selection, or specific shield type names
+
+## Usage Example
+
+```javascript
+const config = {
+ culturesInput: 8,
+ culturesInSetNumber: 15,
+ culturesSet: "european",
+ graphWidth: 2048,
+ graphHeight: 1024,
+ sizeVariety: 1.2,
+ neutralRate: 1.0,
+ emblemShape: "random"
+};
+
+const result = generate(pack, grid, config, utils);
+```
+
+## Notes
+- All numeric values should be validated before passing to ensure they are valid numbers
+- The `graphWidth` and `graphHeight` should match the actual map dimensions
+- Optional properties will use sensible defaults if not provided
+- The config object enables the engine to be completely independent of DOM/UI elements
\ No newline at end of file
diff --git a/procedural/src/engine/support/cultures-generator_external.md b/procedural/src/engine/support/cultures-generator_external.md
new file mode 100644
index 00000000..d2ce3018
--- /dev/null
+++ b/procedural/src/engine/support/cultures-generator_external.md
@@ -0,0 +1,38 @@
+# External Dependencies for cultures-generator.js
+
+The refactored `cultures-generator.js` module requires the following external dependencies to be imported or passed via the `utils` object:
+
+## Core Utility Functions
+- `TIME` - Boolean flag for timing operations
+- `WARN` - Boolean flag for warning messages
+- `ERROR` - Boolean flag for error messages
+- `rand(max)` - Random number generator function
+- `rn(value, precision)` - Round number function
+- `P(probability)` - Probability function
+- `minmax(value, min, max)` - Min/max clamp function
+- `biased(min, max, bias)` - Biased random function
+- `rw(array)` - Random weighted selection function
+- `abbreviate(name, existingCodes)` - Name abbreviation function
+
+## External Modules/Objects
+- `d3` - D3.js library (specifically `d3.quadtree()`, `d3.max()`, `d3.range()`)
+- `Names` - Names generation module with methods:
+ - `Names.getNameBases()`
+ - `Names.getCulture(culture, min, max, suffix)`
+ - `Names.getBase(base, min, max, suffix, index)`
+ - `Names.getBaseShort(index)`
+- `COA` - Coat of Arms data object with shield types:
+ - `COA.shields.types`
+ - `COA.shields[type]`
+- `FlatQueue` - Priority queue implementation
+- `biomesData` - Biome cost data object with `cost` array
+- `nameBases` - Array of name bases
+
+## Data Structures
+- `grid` - Grid data structure with `cells.temp` array
+- `getRandomColor()` - Function to generate random colors (optional utility)
+
+## Notes
+- All these dependencies should be passed through the `utils` parameter to maintain the module's pure, headless nature
+- The `grid` parameter should be passed separately as it's core map data
+- Some utility functions like `getRandomColor` may need to be implemented if not available in the existing codebase
\ No newline at end of file
diff --git a/procedural/src/engine/support/cultures-generator_prompt.md b/procedural/src/engine/support/cultures-generator_prompt.md
new file mode 100644
index 00000000..f7125aee
--- /dev/null
+++ b/procedural/src/engine/support/cultures-generator_prompt.md
@@ -0,0 +1,702 @@
+# cultures-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `cultures-generator.js`.
+
+**File Content:**
+```javascript
+"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.
+ No cultures, states and burgs will be created.
+ 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.
+ Only ${count} out of ${culturesInput.value} requested cultures will be generated.
+ 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};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./cultures-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./cultures-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in cultures-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into cultures-generator_render.md
diff --git a/procedural/src/engine/support/cultures-generator_render.md b/procedural/src/engine/support/cultures-generator_render.md
new file mode 100644
index 00000000..a88f3e5f
--- /dev/null
+++ b/procedural/src/engine/support/cultures-generator_render.md
@@ -0,0 +1,99 @@
+# Removed Rendering/UI Logic from cultures-generator.js
+
+The following UI and rendering logic was removed from the legacy `cultures-generator.js` and needs to be implemented in the Viewer/Client application:
+
+## Alert Dialog System
+
+### Extreme Climate Warning Dialog
+**Location:** Lines 96-109 in original code
+**Removed Code:**
+```javascript
+alertMessage.innerHTML = /* html */ `The climate is harsh and people cannot live in this world.
+ No cultures, states and burgs will be created.
+ Please consider changing climate settings in the World Configurator`;
+
+$("#alert").dialog({
+ resizable: false,
+ title: "Extreme climate warning",
+ buttons: {
+ Ok: function () {
+ $(this).dialog("close");
+ }
+ }
+});
+```
+
+**Replacement Strategy:** The engine now returns an error object with type `"extreme_climate"` that the UI can use to display the appropriate dialog.
+
+### Insufficient Population Warning Dialog
+**Location:** Lines 112-124 in original code
+**Removed Code:**
+```javascript
+alertMessage.innerHTML = /* html */ ` There are only ${populated.length} populated cells and it's insufficient livable area.
+ Only ${count} out of ${culturesInput.value} requested cultures will be generated.
+ Please consider changing climate settings in the World Configurator`;
+$("#alert").dialog({
+ resizable: false,
+ title: "Extreme climate warning",
+ buttons: {
+ Ok: function () {
+ $(this).dialog("close");
+ }
+ }
+});
+```
+
+**Replacement Strategy:** The engine can return a warning object that the UI can use to display this information.
+
+## DOM Element Access
+
+### Removed DOM Queries
+All `byId()` and `document.getElementById()` calls were removed:
+
+1. **`byId("culturesInput").value`** → Replaced with `config.culturesInput`
+2. **`byId("culturesSet").selectedOptions[0].dataset.max`** → Replaced with `config.culturesInSetNumber`
+3. **`byId("culturesSet").value`** → Replaced with `config.culturesSet`
+4. **`byId("sizeVariety").value`** → Replaced with `config.sizeVariety`
+5. **`document.getElementById("emblemShape").value`** → Replaced with `config.emblemShape`
+6. **`byId("neutralRate")?.valueAsNumber`** → Replaced with `config.neutralRate`
+
+### Additional DOM Reference Removed
+- **`culturesInput.value`** (Line 113) → Should use `config.culturesInput`
+
+## Implementation Notes for Viewer/Client
+
+### Error Handling
+The refactored engine returns structured error/warning information instead of directly showing UI dialogs:
+
+```javascript
+// Example error return structure
+{
+ cultures: [...],
+ cells: { culture: [...] },
+ error: {
+ type: "extreme_climate",
+ message: "The climate is harsh...",
+ populated: 150
+ }
+}
+```
+
+### Warning Handling
+For non-fatal warnings (insufficient population), the Viewer should:
+1. Check if the returned culture count is less than requested
+2. Display appropriate warning message to user
+3. Allow user to proceed or modify settings
+
+### Dialog Implementation
+The Viewer should implement:
+1. **Alert dialog system** using modern UI framework (React/Vue/etc.) instead of jQuery UI
+2. **Error message formatting** with HTML support for multi-line messages
+3. **User confirmation handling** for proceeding with warnings
+4. **Settings modification links** to help users fix configuration issues
+
+### Configuration Reading
+The Viewer is responsible for:
+1. Reading all DOM input values
+2. Assembling the `config` object
+3. Passing the config to the engine
+4. Handling any validation of config values before engine calls
\ No newline at end of file
diff --git a/procedural/src/engine/support/features.txt b/procedural/src/engine/support/features.txt
new file mode 100644
index 00000000..d3422558
--- /dev/null
+++ b/procedural/src/engine/support/features.txt
@@ -0,0 +1,302 @@
+"use strict";
+
+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 = utils.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
+export function markupGrid(grid, config, utils) {
+ const {TIME, seed, aleaPRNG} = config;
+ const {rn} = utils;
+
+ 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});
+
+ const updatedGrid = {
+ ...grid,
+ cells: {
+ ...grid.cells,
+ t: distanceField,
+ f: featureIds
+ },
+ features
+ };
+
+ TIME && console.timeEnd("markupGrid");
+
+ return updatedGrid;
+}
+
+// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
+export function markupPack(pack, grid, config, utils, modules) {
+ const {TIME} = config;
+ const {isLand, isWater, dist2, rn, clipPoly, unique, createTypedArray, connectVertices} = utils;
+ const {Lakes} = modules;
+ const {d3} = utils;
+
+ TIME && console.time("markupPack");
+
+ const {cells, vertices} = pack;
+ const {c: neighbors, b: borderCells, i} = cells;
+ const packCellsNumber = i.length;
+ if (!packCellsNumber) return pack; // 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
+
+ const updatedPack = {
+ ...pack,
+ cells: {
+ ...pack.cells,
+ t: distanceField,
+ f: featureIds,
+ haven,
+ harbor
+ },
+ features
+ };
+
+ TIME && console.timeEnd("markupPack");
+
+ return updatedPack;
+
+ 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
+export function specify(pack, grid, modules) {
+ const {Lakes} = modules;
+ 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;
+
+ const updatedFeatures = pack.features.map(feature => {
+ if (!feature || feature.type === "ocean") return feature;
+
+ const updatedFeature = {
+ ...feature,
+ group: defineGroup(feature)
+ };
+
+ if (feature.type === "lake") {
+ updatedFeature.height = Lakes.getHeight(feature);
+ updatedFeature.name = Lakes.getName(feature);
+ }
+
+ return updatedFeature;
+ });
+
+ return {
+ ...pack,
+ features: updatedFeatures
+ };
+
+ function defineGroup(feature) {
+ if (feature.type === "island") return defineIslandGroup(feature);
+ if (feature.type === "ocean") return defineOceanGroup(feature);
+ 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";
+ }
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/features_config.md b/procedural/src/engine/support/features_config.md
new file mode 100644
index 00000000..f17c4aac
--- /dev/null
+++ b/procedural/src/engine/support/features_config.md
@@ -0,0 +1,37 @@
+# Configuration Properties for features.js
+
+## Analysis Results
+
+After thorough analysis of the original `features.js` code, **no DOM-based configuration parameters were found**.
+
+The original module does not contain any `byId()` calls or direct DOM reads that would need to be replaced with `config` object properties.
+
+## Configuration Object Structure
+
+The `config` object passed to the features module functions should contain:
+
+```javascript
+const config = {
+ // Timing and debugging
+ TIME: boolean, // Enable/disable console timing logs
+
+ // Randomization
+ seed: string|number, // Random seed for reproducible generation
+ aleaPRNG: function // Pseudo-random number generator function
+};
+```
+
+## Notes
+
+- This module is purely computational and operates entirely on the provided data structures
+- All configuration is derived from the input `grid` and `pack` objects or calculated dynamically
+- No user interface input values are required for this module's operation
+- The module focuses on geometric and topological analysis of map features rather than user-configurable parameters
+
+## Comparison with Other Modules
+
+Unlike modules such as `burgs-and-states.js` which read values like `statesNumber` from the DOM, the `features.js` module:
+- Does not read any DOM elements
+- Does not require user input parameters
+- Operates purely on mathematical calculations and data structure analysis
+- Uses only internally calculated constants and thresholds
\ No newline at end of file
diff --git a/procedural/src/engine/support/features_external.md b/procedural/src/engine/support/features_external.md
new file mode 100644
index 00000000..b3cc612a
--- /dev/null
+++ b/procedural/src/engine/support/features_external.md
@@ -0,0 +1,51 @@
+# External Dependencies for features.js
+
+The refactored `features.js` module requires the following external dependencies to be imported:
+
+## Module Dependencies
+
+### `Lakes` module
+- **Functions used:**
+ - `Lakes.getHeight(feature)` - Calculate height for lake features
+ - `Lakes.getName(feature)` - Generate names for lake features
+
+## Utility Functions Required
+
+The following utility functions need to be passed via the `utils` parameter:
+
+### Core Utilities
+- `INT8_MAX` - Maximum value for Int8 arrays
+- `rn(value)` - Rounding function
+- `isLand(cellId)` - Check if a cell is land
+- `isWater(cellId)` - Check if a cell is water
+- `dist2(point1, point2)` - Calculate squared distance between two points
+- `clipPoly(vertices)` - Clip polygon vertices
+- `unique(array)` - Remove duplicates from array
+- `createTypedArray({maxValue, length})` - Create appropriately typed array
+- `connectVertices({vertices, startingVertex, ofSameType, closeRing})` - Connect vertices to form paths
+
+### D3.js Integration
+- `d3.polygonArea(points)` - Calculate polygon area (accessed via `utils.d3.polygonArea`)
+
+## Configuration Dependencies
+
+The following configuration values need to be passed via the `config` parameter:
+
+### Timing and Randomization
+- `TIME` - Boolean flag to enable/disable timing logs
+- `seed` - Random seed value for reproducible generation
+- `aleaPRNG` - Pseudo-random number generator function
+
+## Module Integration
+
+The module should be imported and used as follows:
+
+```javascript
+import { markupGrid, markupPack, specify } from './features.js';
+import { Lakes } from './lakes.js';
+
+// Usage example
+const updatedGrid = markupGrid(grid, config, utils);
+const updatedPack = markupPack(pack, grid, config, utils, { Lakes });
+const finalPack = specify(updatedPack, grid, { Lakes });
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/features_prompt.md b/procedural/src/engine/support/features_prompt.md
new file mode 100644
index 00000000..99d32697
--- /dev/null
+++ b/procedural/src/engine/support/features_prompt.md
@@ -0,0 +1,353 @@
+# features.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `features.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Features = (function () {
+ const DEEPER_LAND = 3;
+ const LANDLOCKED = 2;
+ const LAND_COAST = 1;
+ const UNMARKED = 0;
+ const WATER_COAST = -1;
+ const DEEP_WATER = -2;
+
+ // calculate distance to coast for every cell
+ function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
+ for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
+ marked = 0;
+ const prevDistance = distance - increment;
+ for (let cellId = 0; cellId < neighbors.length; cellId++) {
+ if (distanceField[cellId] !== prevDistance) continue;
+
+ for (const neighborId of neighbors[cellId]) {
+ if (distanceField[neighborId] !== UNMARKED) continue;
+ distanceField[neighborId] = distance;
+ marked++;
+ }
+ }
+ }
+ }
+
+ // mark Grid features (ocean, lakes, islands) and calculate distance field
+ function markupGrid() {
+ TIME && console.time("markupGrid");
+ Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
+
+ const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
+ const cellsNumber = i.length;
+ const distanceField = new Int8Array(cellsNumber); // gird.cells.t
+ const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
+ const features = [0];
+
+ const queue = [0];
+ for (let featureId = 1; queue[0] !== -1; featureId++) {
+ const firstCell = queue[0];
+ featureIds[firstCell] = featureId;
+
+ const land = heights[firstCell] >= 20;
+ let border = false; // set true if feature touches map edge
+
+ while (queue.length) {
+ const cellId = queue.pop();
+ if (!border && borderCells[cellId]) border = true;
+
+ for (const neighborId of neighbors[cellId]) {
+ const isNeibLand = heights[neighborId] >= 20;
+
+ if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
+ featureIds[neighborId] = featureId;
+ queue.push(neighborId);
+ } else if (land && !isNeibLand) {
+ distanceField[cellId] = LAND_COAST;
+ distanceField[neighborId] = WATER_COAST;
+ }
+ }
+ }
+
+ const type = land ? "island" : border ? "ocean" : "lake";
+ features.push({i: featureId, land, border, type});
+
+ queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
+ }
+
+ // markup deep ocean cells
+ markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
+
+ grid.cells.t = distanceField;
+ grid.cells.f = featureIds;
+ grid.features = features;
+
+ TIME && console.timeEnd("markupGrid");
+ }
+
+ // mark Pack features (ocean, lakes, islands), calculate distance field and add properties
+ function markupPack() {
+ TIME && console.time("markupPack");
+
+ const {cells, vertices} = pack;
+ const {c: neighbors, b: borderCells, i} = cells;
+ const packCellsNumber = i.length;
+ if (!packCellsNumber) return; // no cells -> there is nothing to do
+
+ const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
+ const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
+ const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
+ const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
+ const features = [0];
+
+ const queue = [0];
+ for (let featureId = 1; queue[0] !== -1; featureId++) {
+ const firstCell = queue[0];
+ featureIds[firstCell] = featureId;
+
+ const land = isLand(firstCell);
+ let border = Boolean(borderCells[firstCell]); // true if feature touches map border
+ let totalCells = 1; // count cells in a feature
+
+ while (queue.length) {
+ const cellId = queue.pop();
+ if (borderCells[cellId]) border = true;
+ if (!border && borderCells[cellId]) border = true;
+
+ for (const neighborId of neighbors[cellId]) {
+ const isNeibLand = isLand(neighborId);
+
+ if (land && !isNeibLand) {
+ distanceField[cellId] = LAND_COAST;
+ distanceField[neighborId] = WATER_COAST;
+ if (!haven[cellId]) defineHaven(cellId);
+ } else if (land && isNeibLand) {
+ if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
+ distanceField[neighborId] = LANDLOCKED;
+ else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
+ distanceField[cellId] = LANDLOCKED;
+ }
+
+ if (!featureIds[neighborId] && land === isNeibLand) {
+ queue.push(neighborId);
+ featureIds[neighborId] = featureId;
+ totalCells++;
+ }
+ }
+ }
+
+ features.push(addFeature({firstCell, land, border, featureId, totalCells}));
+ queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
+ }
+
+ markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
+ markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
+
+ pack.cells.t = distanceField;
+ pack.cells.f = featureIds;
+ pack.cells.haven = haven;
+ pack.cells.harbor = harbor;
+ pack.features = features;
+
+ TIME && console.timeEnd("markupPack");
+
+ function defineHaven(cellId) {
+ const waterCells = neighbors[cellId].filter(isWater);
+ const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
+ const closest = distances.indexOf(Math.min.apply(Math, distances));
+
+ haven[cellId] = waterCells[closest];
+ harbor[cellId] = waterCells.length;
+ }
+
+ function addFeature({firstCell, land, border, featureId, totalCells}) {
+ const type = land ? "island" : border ? "ocean" : "lake";
+ const [startCell, featureVertices] = getCellsData(type, firstCell);
+ const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
+ const area = d3.polygonArea(points); // feature perimiter area
+ const absArea = Math.abs(rn(area));
+
+ const feature = {
+ i: featureId,
+ type,
+ land,
+ border,
+ cells: totalCells,
+ firstCell: startCell,
+ vertices: featureVertices,
+ area: absArea
+ };
+
+ if (type === "lake") {
+ if (area > 0) feature.vertices = feature.vertices.reverse();
+ feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
+ feature.height = Lakes.getHeight(feature);
+ }
+
+ return feature;
+
+ function getCellsData(featureType, firstCell) {
+ if (featureType === "ocean") return [firstCell, []];
+
+ const getType = cellId => featureIds[cellId];
+ const type = getType(firstCell);
+ const ofSameType = cellId => getType(cellId) === type;
+ const ofDifferentType = cellId => getType(cellId) !== type;
+
+ const startCell = findOnBorderCell(firstCell);
+ const featureVertices = getFeatureVertices(startCell);
+ return [startCell, featureVertices];
+
+ function findOnBorderCell(firstCell) {
+ const isOnBorder = cellId => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
+ if (isOnBorder(firstCell)) return firstCell;
+
+ const startCell = cells.i.filter(ofSameType).find(isOnBorder);
+ if (startCell === undefined)
+ throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
+
+ return startCell;
+ }
+
+ function getFeatureVertices(startCell) {
+ const startingVertex = cells.v[startCell].find(v => vertices.c[v].some(ofDifferentType));
+ if (startingVertex === undefined)
+ throw new Error(`Markup: startingVertex for cell ${startCell} is not found`);
+
+ return connectVertices({vertices, startingVertex, ofSameType, closeRing: false});
+ }
+ }
+ }
+ }
+
+ // add properties to pack features
+ function specify() {
+ const gridCellsNumber = grid.cells.i.length;
+ const OCEAN_MIN_SIZE = gridCellsNumber / 25;
+ const SEA_MIN_SIZE = gridCellsNumber / 1000;
+ const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
+ const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
+
+ for (const feature of pack.features) {
+ if (!feature || feature.type === "ocean") continue;
+
+ feature.group = defineGroup(feature);
+
+ if (feature.type === "lake") {
+ feature.height = Lakes.getHeight(feature);
+ feature.name = Lakes.getName(feature);
+ }
+ }
+
+ function defineGroup(feature) {
+ if (feature.type === "island") return defineIslandGroup(feature);
+ if (feature.type === "ocean") return defineOceanGroup();
+ if (feature.type === "lake") return defineLakeGroup(feature);
+ throw new Error(`Markup: unknown feature type ${feature.type}`);
+ }
+
+ function defineOceanGroup(feature) {
+ if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
+ if (feature.cells > SEA_MIN_SIZE) return "sea";
+ return "gulf";
+ }
+
+ function defineIslandGroup(feature) {
+ const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
+ if (prevFeature && prevFeature.type === "lake") return "lake_island";
+ if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
+ if (feature.cells > ISLAND_MIN_SIZE) return "island";
+ return "isle";
+ }
+
+ function defineLakeGroup(feature) {
+ if (feature.temp < -3) return "frozen";
+ if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
+
+ if (!feature.inlets && !feature.outlet) {
+ if (feature.evaporation > feature.flux * 4) return "dry";
+ if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
+ }
+
+ if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
+
+ return "freshwater";
+ }
+ }
+
+ return {markupGrid, markupPack, specify};
+})();
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./features.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./features_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in features_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into features_render.md
diff --git a/procedural/src/engine/support/features_render.md b/procedural/src/engine/support/features_render.md
new file mode 100644
index 00000000..c7b1ccb1
--- /dev/null
+++ b/procedural/src/engine/support/features_render.md
@@ -0,0 +1,46 @@
+# Removed Rendering/UI Logic from features.js
+
+## Analysis Results
+
+After thorough analysis of the original `features.js` code, **no DOM manipulation or SVG rendering logic was found**.
+
+## What Was Analyzed
+
+The analysis looked for the following types of rendering/UI code:
+- `d3.select()` calls for DOM manipulation
+- `document.getElementById()` or similar DOM queries
+- Direct DOM element creation (e.g., creating `` elements)
+- SVG rendering commands
+- Direct DOM property assignments (e.g., `element.innerHTML = ...`)
+- Canvas drawing operations
+
+## Findings
+
+The `features.js` module is purely computational and contains:
+- Mathematical calculations for distance fields
+- Geometric analysis of map features (islands, lakes, oceans)
+- Data structure operations and transformations
+- Feature classification and property assignment
+
+## No Rendering Logic Removed
+
+**No code blocks were removed** from the original module because:
+- The module does not contain any rendering or DOM manipulation code
+- All functionality is related to data processing and analysis
+- The module operates entirely on data structures without visual output
+
+## Viewer Application Responsibilities
+
+Since no rendering logic was present in the original module, the Viewer application will need to implement its own rendering logic for:
+- Visualizing the calculated distance fields
+- Rendering feature boundaries and classifications
+- Displaying feature properties and labels
+- Creating SVG paths for islands, lakes, and ocean features
+
+## Module Purity
+
+This module exemplifies the ideal separation of concerns where:
+- **Engine**: Pure computational logic (this module)
+- **Viewer**: All rendering and visualization (to be implemented separately)
+
+The refactored module maintains this separation by focusing exclusively on data generation and analysis.
\ No newline at end of file
diff --git a/procedural/src/engine/support/fonts_config.md b/procedural/src/engine/support/fonts_config.md
new file mode 100644
index 00000000..179f84cf
--- /dev/null
+++ b/procedural/src/engine/support/fonts_config.md
@@ -0,0 +1,20 @@
+# Config Properties for fonts.js
+
+## Config Properties Identified: None
+
+The original `fonts.js` module did **not contain any DOM reads** that required configuration properties.
+
+## Analysis
+
+The original code had the following characteristics:
+- No `byId()` calls to read DOM input values
+- No configuration parameters read from UI elements
+- All font data was hardcoded in the fonts array
+- Functions operated on provided data parameters rather than reading global configuration
+
+## Notes
+
+- The refactored module is purely data-driven and functional
+- All necessary data is passed as function parameters (e.g., `svgData` in `getUsedFonts()`)
+- No configuration object is needed for this module
+- The module provides utility functions that work with any font data provided to them
\ No newline at end of file
diff --git a/procedural/src/engine/support/fonts_external.md b/procedural/src/engine/support/fonts_external.md
new file mode 100644
index 00000000..20e6c960
--- /dev/null
+++ b/procedural/src/engine/support/fonts_external.md
@@ -0,0 +1,16 @@
+# External Dependencies for fonts.js
+
+The refactored `fonts.js` module has **no external engine dependencies**.
+
+## Imports Required: None
+
+The fonts module is completely self-contained and only depends on:
+- Standard JavaScript APIs (fetch, Promise, FileReader)
+- Browser APIs (when running in browser environment)
+
+## Notes
+
+- The module provides pure utility functions for font management
+- All font data is embedded directly in the module
+- No dependencies on other engine modules like Names, COA, etc.
+- Network requests are handled internally via fetch API for Google Fonts
\ No newline at end of file
diff --git a/procedural/src/engine/support/fonts_prompt.md b/procedural/src/engine/support/fonts_prompt.md
new file mode 100644
index 00000000..da7ff325
--- /dev/null
+++ b/procedural/src/engine/support/fonts_prompt.md
@@ -0,0 +1,477 @@
+# fonts.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `fonts.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+const fonts = [
+ {family: "Arial"},
+ {family: "Brush Script MT"},
+ {family: "Century Gothic"},
+ {family: "Comic Sans MS"},
+ {family: "Copperplate"},
+ {family: "Courier New"},
+ {family: "Garamond"},
+ {family: "Georgia"},
+ {family: "Herculanum"},
+ {family: "Impact"},
+ {family: "Papyrus"},
+ {family: "Party LET"},
+ {family: "Times New Roman"},
+ {family: "Verdana"},
+ {
+ family: "Almendra SC",
+ src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Amarante",
+ src: "url(https://fonts.gstatic.com/s/amarante/v22/xMQXuF1KTa6EvGx9bp-wAXs.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Amatic SC",
+ src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Arima Madurai",
+ src: "url(https://fonts.gstatic.com/s/arimamadurai/v14/t5tmIRoeKYORG0WNMgnC3seB3T7Prw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Architects Daughter",
+ src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Bitter",
+ src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Caesar Dressing",
+ src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Cinzel",
+ src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Dancing Script",
+ src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Eagle Lake",
+ src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Faster One",
+ src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Forum",
+ src: "url(https://fonts.gstatic.com/s/forum/v16/6aey4Ky-Vb8Ew8IROpI.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Fredericka the Great",
+ src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Gloria Hallelujah",
+ src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Great Vibes",
+ src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Henny Penny",
+ src: "url(https://fonts.gstatic.com/s/hennypenny/v17/wXKvE3UZookzsxz_kjGSfPQtvXI.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "IM Fell English",
+ src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Kelly Slab",
+ src: "url(https://fonts.gstatic.com/s/kellyslab/v15/-W_7XJX0Rz3cxUnJC5t6fkQLfg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Kranky",
+ src: "url(https://fonts.gstatic.com/s/kranky/v24/hESw6XVgJzlPsFn8oR2F.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Lobster Two",
+ src: "url(https://fonts.gstatic.com/s/lobstertwo/v18/BngMUXZGTXPUvIoyV6yN5-fN5qU.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Lugrasimo",
+ src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Kaushan Script",
+ src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Macondo",
+ src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "MedievalSharp",
+ src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Metal Mania",
+ src: "url(https://fonts.gstatic.com/s/metalmania/v22/RWmMoKWb4e8kqMfBUdPFJdXFiaQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Metamorphous",
+ src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Montez",
+ src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Nova Script",
+ src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Orbitron",
+ src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Oregano",
+ src: "url(https://fonts.gstatic.com/s/oregano/v13/If2IXTPxciS3H4S2oZDVPg.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Pirata One",
+ src: "url(https://fonts.gstatic.com/s/pirataone/v22/I_urMpiDvgLdLh0fAtofhi-Org.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Sail",
+ src: "url(https://fonts.gstatic.com/s/sail/v16/DPEjYwiBxwYJJBPJAQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Satisfy",
+ src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Shadows Into Light",
+ src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ },
+ {
+ family: "Tapestry",
+ src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Uncial Antiqua",
+ src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Underdog",
+ src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "UnifrakturMaguntia",
+ src: "url(https://fonts.gstatic.com/s/unifrakturmaguntia/v16/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
+ },
+ {
+ family: "Yellowtail",
+ src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
+ unicodeRange:
+ "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
+ }
+];
+
+declareDefaultFonts(); // execute once on load
+
+function declareFont(font) {
+ const {family, src, ...rest} = font;
+ addFontOption(family);
+
+ if (!src) return;
+ const fontFace = new FontFace(family, src, {...rest, display: "block"});
+ document.fonts.add(fontFace);
+}
+
+function declareDefaultFonts() {
+ fonts.forEach(font => declareFont(font));
+}
+
+function getUsedFonts(svg) {
+ const usedFontFamilies = new Set();
+
+ const labelGroups = svg.querySelectorAll("#labels g");
+ for (const labelGroup of labelGroups) {
+ const font = labelGroup.getAttribute("font-family");
+ if (font) usedFontFamilies.add(font);
+ }
+
+ const provinceFont = provs.attr("font-family");
+ if (provinceFont) usedFontFamilies.add(provinceFont);
+
+ const legend = svg.querySelector("#legend");
+ const legendFont = legend?.getAttribute("font-family");
+ if (legendFont) usedFontFamilies.add(legendFont);
+
+ const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
+ return usedFonts;
+}
+
+function addFontOption(family) {
+ const options = document.getElementById("styleSelectFont");
+ const option = document.createElement("option");
+ option.value = family;
+ option.innerText = family;
+ option.style.fontFamily = family;
+ options.add(option);
+}
+
+async function fetchGoogleFont(family) {
+ const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
+ try {
+ const resp = await fetch(url);
+ const text = await resp.text();
+
+ const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
+ const fonts = fontFaceRules.map(fontFace => {
+ const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
+ const src = `url(${srcURL})`;
+ const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
+ const variant = fontFace.match(/font-style: (.*?);/)?.[1];
+
+ const font = {family, src};
+ if (unicodeRange) font.unicodeRange = unicodeRange;
+ if (variant && variant !== "normal") font.variant = variant;
+ return font;
+ });
+
+ return fonts;
+ } catch (err) {
+ ERROR && console.error(err);
+ return null;
+ }
+}
+
+function readBlobAsDataURL(blob) {
+ return new Promise(function (resolve, reject) {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+}
+
+async function loadFontsAsDataURI(fonts) {
+ const promises = fonts.map(async font => {
+ const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
+ const resp = await fetch(url);
+ const blob = await resp.blob();
+ const dataURL = await readBlobAsDataURL(blob);
+
+ return {...font, src: `url('${dataURL}')`};
+ });
+
+ return await Promise.all(promises);
+}
+
+async function addGoogleFont(family) {
+ const fontRanges = await fetchGoogleFont(family);
+ if (!fontRanges) return tip("Cannot fetch Google font for this value", true, "error", 4000);
+ tip(`Google font ${family} is loading...`, true, "warn", 4000);
+
+ const promises = fontRanges.map(range => {
+ const {src, unicodeRange, variant} = range;
+ const fontFace = new FontFace(family, src, {unicodeRange, variant, display: "block"});
+ return fontFace.load();
+ });
+
+ Promise.all(promises)
+ .then(fontFaces => {
+ fontFaces.forEach(fontFace => document.fonts.add(fontFace));
+ fonts.push(...fontRanges);
+ tip(`Google font ${family} is added to the list`, true, "success", 4000);
+ addFontOption(family);
+ document.getElementById("styleSelectFont").value = family;
+ changeFont();
+ })
+ .catch(err => {
+ tip(`Failed to load Google font ${family}`, true, "error", 4000);
+ ERROR && console.error(err);
+ });
+}
+
+function addLocalFont(family) {
+ fonts.push({family});
+
+ const fontFace = new FontFace(family, `local(${family})`, {display: "block"});
+ document.fonts.add(fontFace);
+ tip(`Local font ${family} is added to the fonts list`, true, "success", 4000);
+ addFontOption(family);
+ document.getElementById("styleSelectFont").value = family;
+ changeFont();
+}
+
+function addWebFont(family, url) {
+ const src = `url('${url}')`;
+ fonts.push({family, src});
+
+ const fontFace = new FontFace(family, src, {display: "block"});
+ document.fonts.add(fontFace);
+ tip(`Font ${family} is added to the list`, true, "success", 4000);
+ addFontOption(family);
+ document.getElementById("styleSelectFont").value = family;
+ changeFont();
+}
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./fonts.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./fonts_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in fonts_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into fonts_render.md
diff --git a/procedural/src/engine/support/fonts_render.md b/procedural/src/engine/support/fonts_render.md
new file mode 100644
index 00000000..22716949
--- /dev/null
+++ b/procedural/src/engine/support/fonts_render.md
@@ -0,0 +1,134 @@
+# Removed Rendering/UI Logic from fonts.js
+
+The following code blocks related to DOM manipulation and UI rendering have been **removed** from the engine module and should be moved to the Viewer application:
+
+## 1. Font Option DOM Manipulation
+
+```javascript
+function addFontOption(family) {
+ const options = document.getElementById("styleSelectFont");
+ const option = document.createElement("option");
+ option.value = family;
+ option.innerText = family;
+ option.style.fontFamily = family;
+ options.add(option);
+}
+```
+
+## 2. DOM Font Registration
+
+```javascript
+function declareFont(font) {
+ const {family, src, ...rest} = font;
+ addFontOption(family); // <- UI logic
+
+ if (!src) return;
+ const fontFace = new FontFace(family, src, {...rest, display: "block"});
+ document.fonts.add(fontFace); // <- Browser-specific DOM API
+}
+
+function declareDefaultFonts() {
+ fonts.forEach(font => declareFont(font)); // <- Uses DOM
+}
+
+// Auto-execution on load
+declareDefaultFonts(); // execute once on load
+```
+
+## 3. SVG DOM Querying
+
+```javascript
+function getUsedFonts(svg) {
+ const usedFontFamilies = new Set();
+
+ // Direct DOM querying - moved to viewer
+ const labelGroups = svg.querySelectorAll("#labels g");
+ for (const labelGroup of labelGroups) {
+ const font = labelGroup.getAttribute("font-family");
+ if (font) usedFontFamilies.add(font);
+ }
+
+ // Global variable access
+ const provinceFont = provs.attr("font-family");
+ if (provinceFont) usedFontFamilies.add(provinceFont);
+
+ // Direct DOM querying
+ const legend = svg.querySelector("#legend");
+ const legendFont = legend?.getAttribute("font-family");
+ if (legendFont) usedFontFamilies.add(legendFont);
+
+ const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
+ return usedFonts;
+}
+```
+
+## 4. UI Notification and Interaction Logic
+
+```javascript
+// From addGoogleFont function
+async function addGoogleFont(family) {
+ const fontRanges = await fetchGoogleFont(family);
+ if (!fontRanges) return tip("Cannot fetch Google font for this value", true, "error", 4000);
+ tip(`Google font ${family} is loading...`, true, "warn", 4000);
+
+ // ... font loading logic ...
+
+ Promise.all(promises)
+ .then(fontFaces => {
+ fontFaces.forEach(fontFace => document.fonts.add(fontFace)); // <- DOM manipulation
+ fonts.push(...fontRanges);
+ tip(`Google font ${family} is added to the list`, true, "success", 4000); // <- UI notification
+ addFontOption(family); // <- DOM manipulation
+ document.getElementById("styleSelectFont").value = family; // <- DOM manipulation
+ changeFont(); // <- UI callback
+ })
+ .catch(err => {
+ tip(`Failed to load Google font ${family}`, true, "error", 4000); // <- UI notification
+ ERROR && console.error(err);
+ });
+}
+
+// From addLocalFont function
+function addLocalFont(family) {
+ fonts.push({family});
+
+ const fontFace = new FontFace(family, `local(${family})`, {display: "block"});
+ document.fonts.add(fontFace); // <- DOM manipulation
+ tip(`Local font ${family} is added to the fonts list`, true, "success", 4000); // <- UI notification
+ addFontOption(family); // <- DOM manipulation
+ document.getElementById("styleSelectFont").value = family; // <- DOM manipulation
+ changeFont(); // <- UI callback
+}
+
+// From addWebFont function
+function addWebFont(family, url) {
+ const src = `url('${url}')`;
+ fonts.push({family, src});
+
+ const fontFace = new FontFace(family, src, {display: "block"});
+ document.fonts.add(fontFace); // <- DOM manipulation
+ tip(`Font ${family} is added to the list`, true, "success", 4000); // <- UI notification
+ addFontOption(family); // <- DOM manipulation
+ document.getElementById("styleSelectFont").value = family; // <- DOM manipulation
+ changeFont(); // <- UI callback
+}
+```
+
+## 5. Global Variable Dependencies
+
+- Access to `provs` global variable
+- Calls to `tip()` function for UI notifications
+- Calls to `changeFont()` function for UI updates
+- Access to `ERROR` global flag
+
+## Summary
+
+All DOM manipulation, UI notification, browser font registration, and SVG querying logic has been removed. The refactored engine module now provides pure data processing functions that the Viewer can use to:
+
+1. Get available fonts
+2. Determine used fonts from SVG data structure
+3. Fetch Google Font definitions
+4. Load fonts as data URIs
+5. Create font definitions
+
+The Viewer application should handle all DOM interactions, UI updates, and browser-specific font registration.
\ No newline at end of file
diff --git a/procedural/src/engine/support/heightmap-generator.txt b/procedural/src/engine/support/heightmap-generator.txt
new file mode 100644
index 00000000..be886329
--- /dev/null
+++ b/procedural/src/engine/support/heightmap-generator.txt
@@ -0,0 +1,542 @@
+"use strict";
+
+export function generate(graph, config, utils) {
+ const { aleaPRNG, createTypedArray, findGridCell, getNumberInRange, lim, minmax, rand, P, d3, heightmapTemplates, TIME } = utils;
+ const { templateId, seed, graphWidth, graphHeight } = config;
+
+ TIME && console.time("defineHeightmap");
+
+ Math.random = aleaPRNG(seed);
+ const isTemplate = templateId in heightmapTemplates;
+ const heights = isTemplate ? fromTemplate(graph, templateId, config, utils) : null;
+
+ TIME && console.timeEnd("defineHeightmap");
+
+ return heights;
+}
+
+export function fromTemplate(graph, id, config, utils) {
+ const { heightmapTemplates } = utils;
+ const templateString = heightmapTemplates[id]?.template || "";
+ const steps = templateString.split("\n");
+
+ if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
+
+ let { heights, blobPower, linePower } = setGraph(graph, utils);
+
+ 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}`);
+ heights = addStep(heights, graph, blobPower, linePower, config, utils, ...elements);
+ }
+
+ return heights;
+}
+
+function setGraph(graph, utils) {
+ const { createTypedArray } = utils;
+ const { cellsDesired, cells, points } = graph;
+ const heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
+ const blobPower = getBlobPower(cellsDesired);
+ const linePower = getLinePower(cellsDesired);
+
+ return { heights, blobPower, linePower };
+}
+
+function addStep(heights, graph, blobPower, linePower, config, utils, tool, a2, a3, a4, a5) {
+ if (tool === "Hill") return addHill(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
+ if (tool === "Pit") return addPit(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
+ if (tool === "Range") return addRange(heights, graph, linePower, config, utils, a2, a3, a4, a5);
+ if (tool === "Trough") return addTrough(heights, graph, linePower, config, utils, a2, a3, a4, a5);
+ if (tool === "Strait") return addStrait(heights, graph, config, utils, a2, a3);
+ if (tool === "Mask") return mask(heights, graph, config, utils, a2);
+ if (tool === "Invert") return invert(heights, graph, config, utils, a2, a3);
+ if (tool === "Add") return modify(heights, a3, +a2, 1);
+ if (tool === "Multiply") return modify(heights, a3, 0, +a2);
+ if (tool === "Smooth") return smooth(heights, graph, utils, a2);
+ return heights;
+}
+
+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;
+}
+
+export function addHill(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
+ const { getNumberInRange, lim, findGridCell } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const y = getPointInRange(rangeY, graphHeight, utils);
+ start = findGridCell(x, y, graph);
+ 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 graph.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]));
+ }
+
+ return heights;
+}
+
+export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
+ const { getNumberInRange, lim, findGridCell } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const y = getPointInRange(rangeY, graphHeight, utils);
+ start = findGridCell(x, y, graph);
+ 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;
+
+ graph.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);
+ });
+ }
+ }
+
+ return heights;
+}
+
+export function addRange(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
+ const { getNumberInRange, lim, findGridCell, d3 } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ const startY = getPointInRange(rangeY, graphHeight, utils);
+
+ 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, graph);
+ endCell = findGridCell(endX, endY, graph);
+ }
+
+ let range = getRange(startCell, endCell);
+
+ // get main ridge
+ function getRange(cur, end) {
+ const range = [cur];
+ const p = graph.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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 => {
+ graph.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 = graph.cells.c[cur][d3.scan(graph.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
+ heights[min] = (heights[cur] * 2 + heights[min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ return heights;
+}
+
+export function addTrough(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
+ const { getNumberInRange, lim, findGridCell, d3 } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ 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, utils);
+ startY = getPointInRange(rangeY, graphHeight, utils);
+ startCell = findGridCell(startX, startY, graph);
+ 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, graph);
+ }
+
+ let range = getRange(startCell, endCell);
+
+ // get main ridge
+ function getRange(cur, end) {
+ const range = [cur];
+ const p = graph.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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 => {
+ graph.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 = graph.cells.c[cur][d3.scan(graph.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;
+ }
+ });
+ }
+
+ return heights;
+}
+
+export function addStrait(heights, graph, config, utils, width, direction = "vertical") {
+ const { getNumberInRange, findGridCell, P } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ width = Math.min(getNumberInRange(width), graph.cellsX / 3);
+ if (width < 1 && P(width)) return heights;
+
+ 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, graph);
+ const end = findGridCell(endX, endY, graph);
+ let range = getRange(start, end);
+ const query = [];
+
+ function getRange(cur, end) {
+ const range = [];
+ const p = graph.points;
+
+ while (cur !== end) {
+ let min = Infinity;
+ graph.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) {
+ graph.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--;
+ }
+
+ return heights;
+}
+
+export function modify(heights, range, add, mult, power) {
+ const { lim } = utils;
+
+ heights = new Uint8Array(heights);
+ 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);
+ });
+
+ return heights;
+}
+
+export function smooth(heights, graph, utils, fr = 2, add = 0) {
+ const { lim, d3 } = utils;
+
+ heights = new Uint8Array(heights);
+ heights = heights.map((h, i) => {
+ const a = [h];
+ graph.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);
+ });
+
+ return heights;
+}
+
+export function mask(heights, graph, config, utils, power = 1) {
+ const { lim } = utils;
+ const { graphWidth, graphHeight } = config;
+
+ heights = new Uint8Array(heights);
+ const fr = power ? Math.abs(power) : 1;
+
+ heights = heights.map((h, i) => {
+ const [x, y] = graph.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);
+ });
+
+ return heights;
+}
+
+export function invert(heights, graph, config, utils, count, axes) {
+ const { P } = utils;
+
+ if (!P(count)) return heights;
+
+ heights = new Uint8Array(heights);
+ const invertX = axes !== "y";
+ const invertY = axes !== "x";
+ const { cellsX, cellsY } = graph;
+
+ 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];
+ });
+
+ return inverted;
+}
+
+function getPointInRange(range, length, utils) {
+ const { rand } = utils;
+
+ if (typeof range !== "string") {
+ 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);
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/heightmap-generator_config.md b/procedural/src/engine/support/heightmap-generator_config.md
new file mode 100644
index 00000000..4206363a
--- /dev/null
+++ b/procedural/src/engine/support/heightmap-generator_config.md
@@ -0,0 +1,18 @@
+# Config Properties for heightmap-generator.js
+
+The refactored heightmap-generator module requires the following configuration properties:
+
+## Core Configuration
+- `templateId` - String identifier for the heightmap template to use (replaces `byId("templateInput").value`)
+- `seed` - Random seed value for deterministic generation
+- `graphWidth` - Width of the graph/map area
+- `graphHeight` - Height of the graph/map area
+
+## Original DOM Reads Converted
+- **`byId("templateInput").value`** → **`config.templateId`**
+ - Used in the `generate()` function to determine which heightmap template or precreated heightmap to use
+
+## Global Variables Now in Config
+- **`graphWidth`** - Previously a global variable, now passed via config
+- **`graphHeight`** - Previously a global variable, now passed via config
+- **`seed`** - Previously a global variable, now passed via config
\ No newline at end of file
diff --git a/procedural/src/engine/support/heightmap-generator_external.md b/procedural/src/engine/support/heightmap-generator_external.md
new file mode 100644
index 00000000..0c8a4cc1
--- /dev/null
+++ b/procedural/src/engine/support/heightmap-generator_external.md
@@ -0,0 +1,25 @@
+# External Dependencies for heightmap-generator.js
+
+The refactored heightmap-generator module requires the following external dependencies to be imported or provided via the `utils` object:
+
+## Utility Functions
+- `aleaPRNG` - Pseudo-random number generator function for seeding
+- `createTypedArray` - Creates typed arrays with specified parameters
+- `findGridCell` - Finds grid cell at given coordinates
+- `getNumberInRange` - Converts range string to numeric value
+- `lim` - Limits/clamps values to valid range
+- `minmax` - Min/max utility function
+- `rand` - Random number generator within range
+- `P` - Probability utility function
+
+## Libraries
+- `d3` - D3.js library methods:
+ - `d3.mean()` - Calculates mean of array
+ - `d3.range()` - Creates array of numbers
+ - `d3.scan()` - Finds index of minimum/maximum element
+
+## Data Objects
+- `heightmapTemplates` - Object containing heightmap template definitions
+
+## Configuration/Global Variables
+- `TIME` - Boolean flag for timing operations
\ No newline at end of file
diff --git a/procedural/src/engine/support/heightmap-generator_prompt.md b/procedural/src/engine/support/heightmap-generator_prompt.md
new file mode 100644
index 00000000..22dec38f
--- /dev/null
+++ b/procedural/src/engine/support/heightmap-generator_prompt.md
@@ -0,0 +1,625 @@
+# heightmap-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `heightmap-generator.js`.
+
+**File Content:**
+```javascript
+"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() {
+ 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
+ };
+})();
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./heightmap-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./heightmap-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in heightmap-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application.
diff --git a/procedural/src/engine/support/heightmap-generator_render.md b/procedural/src/engine/support/heightmap-generator_render.md
new file mode 100644
index 00000000..14b36944
--- /dev/null
+++ b/procedural/src/engine/support/heightmap-generator_render.md
@@ -0,0 +1,18 @@
+# Removed Rendering/UI Logic
+
+The following DOM/browser-dependent code was completely removed:
+- **fromPrecreated() function** - Created DOM canvas and image elements, used document.createElement(), canvas context manipulation, and image loading
+- **Canvas manipulation code** - canvas.width/height, ctx.drawImage(), ctx.getImageData()
+- **Image loading logic** - new Image(), img.src, img.onload event handling
+- **DOM element removal** - canvas.remove(), img.remove()
+
+## Future Work Required
+
+The `fromPrecreated()` function has been replaced with a placeholder that throws an error. To make this work in a headless environment, the following will be needed:
+
+1. **Image Loading Utility** - A `utils.loadImage()` function that can load PNG files in any JavaScript environment
+2. **Image Processing Library** - For Node.js environments, a library like the `canvas` package to process image data
+3. **Refactored getHeightsFromImageData()** - This function needs to be updated to work with headless image processing
+4. **Environment Detection** - Logic to determine whether to use browser APIs or Node.js alternatives
+
+Currently, attempting to generate heightmaps from precreated PNG files will throw an error indicating this functionality requires further implementation.
\ No newline at end of file
diff --git a/procedural/src/engine/support/lakes_config.md b/procedural/src/engine/support/lakes_config.md
new file mode 100644
index 00000000..03b0de71
--- /dev/null
+++ b/procedural/src/engine/support/lakes_config.md
@@ -0,0 +1,44 @@
+# Config Properties for lakes.js
+
+The refactored `lakes.js` module identified the following DOM reads that have been converted to config properties:
+
+## Config Properties
+
+### 1. lakeElevationLimit
+- **Original DOM read:** `+byId("lakeElevationLimitOutput").value`
+- **Config property:** `config.lakeElevationLimit`
+- **Used in:** `detectCloseLakes()` function
+- **Type:** Number
+- **Description:** The elevation limit used to determine if a lake can be potentially open (not in deep depression)
+
+### 2. heightExponent
+- **Original DOM read:** `heightExponentInput.value`
+- **Config property:** `config.heightExponent`
+- **Used in:** `defineClimateData()` function (specifically in `getLakeEvaporation()`)
+- **Type:** Number
+- **Description:** The height exponent used in evaporation calculations for lakes
+
+## Config Object Structure
+
+```javascript
+const config = {
+ lakeElevationLimit: 50, // Example value - was read from "lakeElevationLimitOutput" element
+ heightExponent: 2 // Example value - was read from "heightExponentInput" element
+};
+```
+
+## Function Signatures
+
+Functions that require the config object:
+
+```javascript
+detectCloseLakes(pack, grid, heights, config)
+defineClimateData(pack, grid, heights, config, utils)
+```
+
+## Notes
+
+- Both config properties are numeric values used in mathematical calculations
+- `lakeElevationLimit` affects lake classification logic
+- `heightExponent` affects evaporation rate calculations
+- These values were previously read directly from DOM input elements
\ No newline at end of file
diff --git a/procedural/src/engine/support/lakes_external.md b/procedural/src/engine/support/lakes_external.md
new file mode 100644
index 00000000..fb3ef9bb
--- /dev/null
+++ b/procedural/src/engine/support/lakes_external.md
@@ -0,0 +1,39 @@
+# External Dependencies for lakes.js
+
+The refactored `lakes.js` module requires the following external modules to be imported:
+
+## Required Imports
+
+1. **Names module** - for generating lake names
+ - Used in: `getName()` function
+ - Dependency: `Names.getCulture(culture)`
+
+## Utility Dependencies
+
+The module also requires utility functions passed via a `utils` object parameter:
+
+1. **d3 utilities**
+ - `d3.min()` - for finding minimum values in arrays
+ - `d3.mean()` - for calculating averages
+ - Used in: `defineClimateData()`, `getHeight()` functions
+
+2. **rn() function** - rounding utility
+ - Used for rounding numerical values to specified decimal places
+ - Used in: `defineClimateData()`, `getHeight()` functions
+
+## Import Structure
+
+```javascript
+import { Names } from './names.js';
+
+// Usage in function calls:
+// defineClimateData(pack, grid, heights, config, { d3, rn })
+// getHeight(feature, pack, { d3, rn })
+// getName(feature, pack, Names)
+```
+
+## Notes
+
+- The `utils` object containing `d3` and `rn` should be passed as function parameters
+- The `Names` module should be imported and passed to the `getName()` function
+- All other dependencies have been eliminated through dependency injection
\ No newline at end of file
diff --git a/procedural/src/engine/support/lakes_prompt.md b/procedural/src/engine/support/lakes_prompt.md
new file mode 100644
index 00000000..2aee438b
--- /dev/null
+++ b/procedural/src/engine/support/lakes_prompt.md
@@ -0,0 +1,200 @@
+# lakes.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `lakes.js`.
+
+**File Content:**
+```javascript
+"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 getName = function (feature) {
+ const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
+ const culture = pack.cells.culture[landCell];
+ return Names.getCulture(culture);
+ };
+
+ return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./lakes.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./lakes_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in lakes_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into lakes_render.md
diff --git a/procedural/src/engine/support/lakes_render.md b/procedural/src/engine/support/lakes_render.md
new file mode 100644
index 00000000..414788b9
--- /dev/null
+++ b/procedural/src/engine/support/lakes_render.md
@@ -0,0 +1,41 @@
+# Removed Rendering/UI Logic from lakes.js
+
+## Analysis Result: No Rendering Logic Found
+
+After careful analysis of the original `lakes.js` module, **no DOM manipulation or SVG rendering logic was found** that needed to be removed.
+
+## Original Code Analysis
+
+The original `lakes.js` module contained only:
+
+1. **Data Processing Functions:**
+ - `detectCloseLakes()` - Pure computational logic for lake classification
+ - `defineClimateData()` - Mathematical calculations for lake climate properties
+ - `cleanupLakeData()` - Data cleanup and filtering operations
+ - `getHeight()` - Mathematical calculation for lake elevation
+ - `getName()` - Name generation using external Names module
+
+2. **DOM Reads Only (No DOM Writes):**
+ - `byId("lakeElevationLimitOutput").value` - Configuration input (converted to config property)
+ - `heightExponentInput.value` - Configuration input (converted to config property)
+
+## No Removed Code Blocks
+
+There were **no code blocks removed** from the original module because:
+
+- No `d3.select()` calls for DOM/SVG manipulation
+- No `document.getElementById().innerHTML` assignments
+- No DOM element creation or modification
+- No SVG path generation or rendering
+- No UI notification calls (like `tip()`)
+- No direct DOM manipulation whatsoever
+
+## Conclusion
+
+The original `lakes.js` module was already focused purely on data processing and mathematical calculations. The only browser dependencies were:
+
+1. DOM reads for configuration (converted to config parameters)
+2. Access to global state variables (converted to dependency injection)
+3. External utility dependencies (converted to injected parameters)
+
+All refactoring work was focused on **dependency injection** and **config parameter extraction** rather than removing rendering logic, as none existed in the original code.
\ No newline at end of file
diff --git a/procedural/src/engine/support/markers-generator_config.md b/procedural/src/engine/support/markers-generator_config.md
new file mode 100644
index 00000000..d9a3cc3f
--- /dev/null
+++ b/procedural/src/engine/support/markers-generator_config.md
@@ -0,0 +1,17 @@
+# Configuration Properties for markers-generator.js
+
+The refactored `markers-generator.js` module requires the following configuration properties to be passed via the `config` object:
+
+## Required Config Properties
+
+### `culturesSet` (string)
+- **Source**: Originally `document.getElementById("culturesSet").value` on line 80
+- **Purpose**: Determines the culture set being used for map generation
+- **Usage**: Used to detect if Fantasy cultures are enabled, which affects multiplier values for fantasy-themed markers like portals, rifts, and disturbed burials
+- **Example**: `"Fantasy European"`, `"Real World"`
+
+## Notes
+
+Only one DOM call was identified in the original code that needed to be converted to a configuration property. The `culturesSet` value is used to determine whether fantasy elements should be included in marker generation by checking if the string contains "Fantasy".
+
+The calling application should read this value from the UI and pass it in the config object when calling the marker generation functions.
\ No newline at end of file
diff --git a/procedural/src/engine/support/markers-generator_external.md b/procedural/src/engine/support/markers-generator_external.md
new file mode 100644
index 00000000..5ac9efee
--- /dev/null
+++ b/procedural/src/engine/support/markers-generator_external.md
@@ -0,0 +1,40 @@
+# External Dependencies for markers-generator.js
+
+The refactored `markers-generator.js` module requires the following external modules to be imported:
+
+## Core Modules
+- `Names` - Used for generating culture-specific names and toponyms
+- `Routes` - Used for checking crossroads, connections, and road availability
+- `BurgsAndStates` - Used for generating campaign data for battlefields
+
+## Utility Functions
+The following utility functions need to be passed in the `utils` object:
+
+### Random/Math Utilities
+- `P(probability)` - Probability function (returns true with given probability)
+- `rw(weights)` - Random weighted selection from object
+- `ra(array)` - Random array element selection
+- `rand(min, max)` - Random integer between min and max
+- `gauss(mean, deviation, min, max)` - Gaussian distribution random number
+- `rn(number)` - Round number function
+- `last(array)` - Get last element of array
+
+### Data Processing
+- `d3` - D3.js library (specifically `d3.mean()` for bridge generation)
+- `getFriendlyHeight(point)` - Convert height coordinates to readable format
+- `convertTemperature(value)` - Temperature conversion utility
+- `getAdjective(name)` - Generate adjective form of name
+- `capitalize(string)` - String capitalization utility
+- `generateDate(start, end)` - Date generation utility
+
+### Global Configuration
+- `populationRate` - Population calculation rate
+- `urbanization` - Urbanization factor
+- `heightUnit` - Height measurement unit object with `.value` property
+- `biomesData` - Biome data object with `.habitability` array
+- `options` - Global options object with `.era` property
+- `seed` - Global random seed
+- `TIME` - Debug timing flag
+
+## Notes
+All external dependencies are injected through function parameters to maintain the engine's environment-agnostic design. The calling code is responsible for providing these dependencies.
\ No newline at end of file
diff --git a/procedural/src/engine/support/markers-generator_prompt.md b/procedural/src/engine/support/markers-generator_prompt.md
new file mode 100644
index 00000000..98cc0175
--- /dev/null
+++ b/procedural/src/engine/support/markers-generator_prompt.md
@@ -0,0 +1,1371 @@
+# markers-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `markers-generator.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Markers = (function () {
+ let config = getDefaultConfig();
+ let occupied = [];
+
+ function getDefaultConfig() {
+ const culturesSet = document.getElementById("culturesSet").value;
+ const isFantasy = culturesSet.includes("Fantasy");
+
+ /*
+ Default markers config:
+ type - short description (snake-case)
+ icon - unicode character or url to image
+ dx: icon offset in x direction, in pixels
+ dy: icon offset in y direction, in pixels
+ min: minimum number of candidates to add at least 1 marker
+ each: how many of the candidates should be added as markers
+ multiplier: multiply markers quantity to add
+ list: function to select candidates
+ add: function to add marker legend
+ */
+ // prettier-ignore
+ return [
+ {type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
+ {type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
+ {type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource},
+ {type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine},
+ {type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge},
+ {type: "inns", icon: "🍻", px: 14, min: 1, each: 10, multiplier: 1, list: listInns, add: addInn},
+ {type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse},
+ {type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall},
+ {type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield},
+ {type: "dungeons", icon: "🗝️", dy: 51, px: 13, min: 30, each: 200, multiplier: 1, list: listDungeons, add: addDungeon},
+ {type: "lake-monsters", icon: "🐉", dy: 48, min: 2, each: 10, multiplier: 1, list: listLakeMonsters, add: addLakeMonster},
+ {type: "sea-monsters", icon: "🦑", min: 50, each: 700, multiplier: 1, list: listSeaMonsters, add: addSeaMonster},
+ {type: "hill-monsters", icon: "👹", dy: 54, px: 13, min: 30, each: 600, multiplier: 1, list: listHillMonsters, add: addHillMonster},
+ {type: "sacred-mountains", icon: "🗻", dy: 48, min: 1, each: 5, multiplier: 1, list: listSacredMountains, add: addSacredMountain},
+ {type: "sacred-forests", icon: "🌳", min: 30, each: 1000, multiplier: 1, list: listSacredForests, add: addSacredForest},
+ {type: "sacred-pineries", icon: "🌲", px: 13, min: 30, each: 800, multiplier: 1, list: listSacredPineries, add: addSacredPinery},
+ {type: "sacred-palm-groves", icon: "🌴", px: 13, min: 1, each: 100, multiplier: 1, list: listSacredPalmGroves, add: addSacredPalmGrove},
+ {type: "brigands", icon: "💰", px: 13, min: 50, each: 100, multiplier: 1, list: listBrigands, add: addBrigands},
+ {type: "pirates", icon: "🏴☠️", dx: 51, min: 40, each: 300, multiplier: 1, list: listPirates, add: addPirates},
+ {type: "statues", icon: "🗿", min: 80, each: 1200, multiplier: 1, list: listStatues, add: addStatue},
+ {type: "ruins", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins},
+ {type: "libraries", icon: "📚", min: 10, each: 1200, multiplier: 1, list: listLibraries, add: addLibrary},
+ {type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuse},
+ {type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJoust},
+ {type: "fairs", icon: "🎠", min: 50, each: 1000, multiplier: 1, list: listFairs, add: addFair},
+ {type: "canoes", icon: "🛶", min: 500, each: 2000, multiplier: 1, list: listCanoes, add: addCanoe},
+ {type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigration},
+ {type: "dances", icon: "💃🏽", min: 50, each: 1000, multiplier: 1, list: listDances, add: addDances},
+ {type: "mirage", icon: "💦", min: 10, each: 400, multiplier: 1, list: listMirage, add: addMirage},
+ {type: "caves", icon:"🦇", min: 60, each: 1000, multiplier: 1, list: listCaves, add: addCave},
+ {type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal},
+ {type: "rifts", icon: "🎆", min: 5, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRift},
+ {type: "disturbed-burials", icon: "💀", min: 20, each: 3000, multiplier: +isFantasy, list: listDisturbedBurial, add: addDisturbedBurial},
+ {type: "necropolises", icon: "🪦", min: 20, each: 1000, multiplier: 1, list: listNecropolis, add: addNecropolis},
+ {type: "encounters", icon: "🧙", min: 10, each: 600, multiplier: 1, list: listEncounters, add: addEncounter},
+ ];
+ }
+
+ const getConfig = () => config;
+
+ const setConfig = newConfig => {
+ config = newConfig;
+ };
+
+ const generate = function () {
+ setConfig(getDefaultConfig());
+ pack.markers = [];
+ generateTypes();
+ };
+
+ const regenerate = () => {
+ pack.markers = pack.markers.filter(({i, lock, cell}) => {
+ if (lock) {
+ occupied[cell] = true;
+ return true;
+ }
+ const id = `marker${i}`;
+ document.getElementById(id)?.remove();
+ const index = notes.findIndex(note => note.id === id);
+ if (index != -1) notes.splice(index, 1);
+ return false;
+ });
+
+ generateTypes();
+ };
+
+ const add = marker => {
+ const base = config.find(c => c.type === marker.type);
+ if (base) {
+ const {icon, type, dx, dy, px} = base;
+ marker = addMarker({icon, type, dx, dy, px}, marker);
+ base.add("marker" + marker.i, marker.cell);
+ return marker;
+ }
+
+ const i = last(pack.markers)?.i + 1 || 0;
+ pack.markers.push({...marker, i});
+ occupied[marker.cell] = true;
+ return {...marker, i};
+ };
+
+ function generateTypes() {
+ TIME && console.time("addMarkers");
+
+ config.forEach(({type, icon, dx, dy, px, min, each, multiplier, list, add}) => {
+ if (multiplier === 0) return;
+
+ let candidates = Array.from(list(pack));
+ let quantity = getQuantity(candidates, min, each, multiplier);
+ // uncomment for debugging:
+ // console.info(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
+
+ while (quantity && candidates.length) {
+ const [cell] = extractAnyElement(candidates);
+ const marker = addMarker({icon, type, dx, dy, px}, {cell});
+ if (!marker) continue;
+ add("marker" + marker.i, cell);
+ quantity--;
+ }
+ });
+
+ occupied = [];
+ TIME && console.timeEnd("addMarkers");
+ }
+
+ function getQuantity(array, min, each, multiplier) {
+ if (!array.length || array.length < min / multiplier) return 0;
+ const requestQty = Math.ceil((array.length / each) * multiplier);
+ return array.length < requestQty ? array.length : requestQty;
+ }
+
+ function extractAnyElement(array) {
+ const index = Math.floor(Math.random() * array.length);
+ return array.splice(index, 1);
+ }
+
+ function getMarkerCoordinates(cell) {
+ const {cells, burgs} = pack;
+ const burgId = cells.burg[cell];
+
+ if (burgId) {
+ const {x, y} = burgs[burgId];
+ return [x, y];
+ }
+
+ return cells.p[cell];
+ }
+
+ function addMarker(base, marker) {
+ if (marker.cell === undefined) return;
+ const i = last(pack.markers)?.i + 1 || 0;
+ const [x, y] = getMarkerCoordinates(marker.cell);
+ marker = {...base, x, y, ...marker, i};
+ pack.markers.push(marker);
+ occupied[marker.cell] = true;
+ return marker;
+ }
+
+ function deleteMarker(markerId) {
+ const noteId = "marker" + markerId;
+ notes = notes.filter(note => note.id !== noteId);
+ pack.markers = pack.markers.filter(m => m.i !== markerId);
+ }
+
+ function listVolcanoes({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70);
+ }
+
+ function addVolcano(id, cell) {
+ const {cells} = pack;
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = P(0.3) ? "Mount " + proper : P(0.7) ? proper + " Volcano" : proper;
+ const status = P(0.6) ? "Dormant" : P(0.4) ? "Active" : "Erupting";
+ notes.push({id, name, legend: `${status} volcano. Height: ${getFriendlyHeight(cells.p[cell])}.`});
+ }
+
+ function listHotSprings({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 50 && cells.culture[i]);
+ }
+
+ function addHotSpring(id, cell) {
+ const {cells} = pack;
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const temp = convertTemperature(gauss(35, 15, 20, 100));
+ const name = P(0.3) ? "Hot Springs of " + proper : P(0.7) ? proper + " Hot Springs" : proper;
+ const legend = `A geothermal springs with naturally heated water that provide relaxation and medicinal benefits. Average temperature is ${temp}.`;
+
+ notes.push({id, name, legend});
+ }
+
+ function listWaterSources({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 30 && cells.r[i]);
+ }
+
+ function addWaterSource(id, cell) {
+ const {cells} = pack;
+
+ const type = rw({
+ "Healing Spring": 5,
+ "Purifying Well": 2,
+ "Enchanted Reservoir": 1,
+ "Creek of Luck": 1,
+ "Fountain of Youth": 1,
+ "Wisdom Spring": 1,
+ "Spring of Life": 1,
+ "Spring of Youth": 1,
+ "Healing Stream": 1
+ });
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = `${proper} ${type}`;
+ const legend =
+ "This legendary water source is whispered about in ancient tales and believed to possess mystical properties. The spring emanates crystal-clear water, shimmering with an otherworldly iridescence that sparkles even in the dimmest light.";
+
+ notes.push({id, name, legend});
+ }
+
+ function listMines({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 47 && cells.burg[i]);
+ }
+
+ function addMine(id, cell) {
+ const {cells} = pack;
+
+ const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
+ const resource = rw(resources);
+ const burg = pack.burgs[cells.burg[cell]];
+ const name = `${burg.name} — ${resource} mining town`;
+ const population = rn(burg.population * populationRate * urbanization);
+ const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine.`;
+ notes.push({id, name, legend});
+ }
+
+ function listBridges({cells, burgs}) {
+ const meanFlux = d3.mean(cells.fl.filter(fl => fl));
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.burg[i] &&
+ cells.t[i] !== 1 &&
+ burgs[cells.burg[i]].population > 20 &&
+ cells.r[i] &&
+ cells.fl[i] > meanFlux
+ );
+ }
+
+ function addBridge(id, cell) {
+ const {cells} = pack;
+
+ const burg = pack.burgs[cells.burg[cell]];
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const name = river && P(0.2) ? `${river.name} Bridge` : `${burg.name} Bridge`;
+ const weightedAdjectives = {
+ stone: 10,
+ wooden: 1,
+ lengthy: 2,
+ formidable: 2,
+ rickety: 1,
+ beaten: 1,
+ weathered: 1
+ };
+ const barriers = [
+ "its collapse during the flood",
+ "being rumoured to attract trolls",
+ "the drying up of local trade",
+ "banditry infested the area",
+ "the old waypoints crumbled"
+ ];
+ const legend = P(0.7)
+ ? `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}.`
+ : `An old crossing of the ${riverName}, rarely used since ${ra(barriers)}.`;
+
+ notes.push({id, name, legend});
+ }
+
+ function listInns({cells}) {
+ const crossRoads = cells.i.filter(i => !occupied[i] && cells.pop[i] > 5 && Routes.isCrossroad(i));
+ return crossRoads;
+ }
+
+ function addInn(id, cell) {
+ const colors = [
+ "Dark",
+ "Light",
+ "Bright",
+ "Golden",
+ "White",
+ "Black",
+ "Red",
+ "Pink",
+ "Purple",
+ "Blue",
+ "Green",
+ "Yellow",
+ "Amber",
+ "Orange",
+ "Brown",
+ "Grey"
+ ];
+ const animals = [
+ "Antelope",
+ "Ape",
+ "Badger",
+ "Bear",
+ "Beaver",
+ "Bison",
+ "Boar",
+ "Buffalo",
+ "Cat",
+ "Crane",
+ "Crocodile",
+ "Crow",
+ "Deer",
+ "Dog",
+ "Eagle",
+ "Elk",
+ "Fox",
+ "Goat",
+ "Goose",
+ "Hare",
+ "Hawk",
+ "Heron",
+ "Horse",
+ "Hyena",
+ "Ibis",
+ "Jackal",
+ "Jaguar",
+ "Lark",
+ "Leopard",
+ "Lion",
+ "Mantis",
+ "Marten",
+ "Moose",
+ "Mule",
+ "Narwhal",
+ "Owl",
+ "Panther",
+ "Rat",
+ "Raven",
+ "Rook",
+ "Scorpion",
+ "Shark",
+ "Sheep",
+ "Snake",
+ "Spider",
+ "Swan",
+ "Tiger",
+ "Turtle",
+ "Wolf",
+ "Wolverine",
+ "Camel",
+ "Falcon",
+ "Hound",
+ "Ox"
+ ];
+ const adjectives = [
+ "New",
+ "Good",
+ "High",
+ "Old",
+ "Great",
+ "Big",
+ "Major",
+ "Happy",
+ "Main",
+ "Huge",
+ "Far",
+ "Beautiful",
+ "Fair",
+ "Prime",
+ "Ancient",
+ "Golden",
+ "Proud",
+ "Lucky",
+ "Fat",
+ "Honest",
+ "Giant",
+ "Distant",
+ "Friendly",
+ "Loud",
+ "Hungry",
+ "Magical",
+ "Superior",
+ "Peaceful",
+ "Frozen",
+ "Divine",
+ "Favorable",
+ "Brave",
+ "Sunny",
+ "Flying"
+ ];
+ const methods = [
+ "Boiled",
+ "Grilled",
+ "Roasted",
+ "Spit-roasted",
+ "Stewed",
+ "Stuffed",
+ "Jugged",
+ "Mashed",
+ "Baked",
+ "Braised",
+ "Poached",
+ "Marinated",
+ "Pickled",
+ "Smoked",
+ "Dried",
+ "Dry-aged",
+ "Corned",
+ "Fried",
+ "Pan-fried",
+ "Deep-fried",
+ "Dressed",
+ "Steamed",
+ "Cured",
+ "Syrupped",
+ "Flame-Broiled"
+ ];
+ const courses = [
+ "beef",
+ "pork",
+ "bacon",
+ "chicken",
+ "lamb",
+ "chevon",
+ "hare",
+ "rabbit",
+ "hart",
+ "deer",
+ "antlers",
+ "bear",
+ "buffalo",
+ "badger",
+ "beaver",
+ "turkey",
+ "pheasant",
+ "duck",
+ "goose",
+ "teal",
+ "quail",
+ "pigeon",
+ "seal",
+ "carp",
+ "bass",
+ "pike",
+ "catfish",
+ "sturgeon",
+ "escallop",
+ "pie",
+ "cake",
+ "pottage",
+ "pudding",
+ "onions",
+ "carrot",
+ "potato",
+ "beet",
+ "garlic",
+ "cabbage",
+ "eggplant",
+ "eggs",
+ "broccoli",
+ "zucchini",
+ "pepper",
+ "olives",
+ "pumpkin",
+ "spinach",
+ "peas",
+ "chickpea",
+ "beans",
+ "rice",
+ "pasta",
+ "bread",
+ "apples",
+ "peaches",
+ "pears",
+ "melon",
+ "oranges",
+ "mango",
+ "tomatoes",
+ "cheese",
+ "corn",
+ "rat tails",
+ "pig ears"
+ ];
+ const types = [
+ "hot",
+ "cold",
+ "fire",
+ "ice",
+ "smoky",
+ "misty",
+ "shiny",
+ "sweet",
+ "bitter",
+ "salty",
+ "sour",
+ "sparkling",
+ "smelly"
+ ];
+ const drinks = [
+ "wine",
+ "brandy",
+ "gin",
+ "whisky",
+ "rom",
+ "beer",
+ "cider",
+ "mead",
+ "liquor",
+ "spirits",
+ "vodka",
+ "tequila",
+ "absinthe",
+ "nectar",
+ "milk",
+ "kvass",
+ "kumis",
+ "tea",
+ "water",
+ "juice",
+ "sap"
+ ];
+
+ const typeName = P(0.3) ? "inn" : "tavern";
+ const isAnimalThemed = P(0.7);
+ const animal = ra(animals);
+ const name = isAnimalThemed
+ ? P(0.6)
+ ? ra(colors) + " " + animal
+ : ra(adjectives) + " " + animal
+ : ra(adjectives) + " " + capitalize(typeName);
+ const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
+ const course = `${ra(methods)} ${meal}`.toLowerCase();
+ const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
+ const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here.`;
+ notes.push({id, name: "The " + name, legend});
+ }
+
+ function listLighthouses({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && Routes.isConnected(c))
+ );
+ }
+
+ function addLighthouse(id, cell) {
+ const {cells} = pack;
+
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({
+ id,
+ name: getAdjective(proper) + " Lighthouse" + name,
+ legend: `A lighthouse to serve as a beacon for ships in the open sea.`
+ });
+ }
+
+ function listWaterfalls({cells}) {
+ return cells.i.filter(
+ i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])
+ );
+ }
+
+ function addWaterfall(id, cell) {
+ const {cells} = pack;
+
+ const descriptions = [
+ "A gorgeous waterfall flows here.",
+ "The rapids of an exceptionally beautiful waterfall.",
+ "An impressive waterfall has cut through the land.",
+ "The cascades of a stunning waterfall.",
+ "A river drops down from a great height forming a wonderous waterfall.",
+ "A breathtaking waterfall cuts through the landscape."
+ ];
+
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `${ra(descriptions)}`});
+ }
+
+ function listBattlefields({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25
+ );
+ }
+
+ function addBattlefield(id, cell) {
+ const {cells, states} = pack;
+
+ const state = states[cells.state[cell]];
+ if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
+ const campaign = ra(state.campaigns);
+ const date = generateDate(campaign.start, campaign.end);
+ const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
+ const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listDungeons({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.pop[i] && cells.pop[i] < 3);
+ }
+
+ function addDungeon(id, cell) {
+ const dungeonSeed = `${seed}${cell}`;
+ const name = "Dungeon";
+ const legend = ``;
+ notes.push({id, name, legend});
+ }
+
+ function listLakeMonsters({features}) {
+ return features
+ .filter(feature => feature.type === "lake" && feature.group === "freshwater" && !occupied[feature.firstCell])
+ .map(feature => feature.firstCell);
+ }
+
+ function addLakeMonster(id, cell) {
+ const lake = pack.features[pack.cells.f[cell]];
+
+ // Check that the feature is a lake in case the user clicked on a wrong
+ // square
+ if (lake.type !== "lake") return;
+
+ const name = `${lake.name} Monster`;
+ const length = gauss(10, 5, 5, 100);
+ const subjects = [
+ "Locals",
+ "Elders",
+ "Inscriptions",
+ "Tipplers",
+ "Legends",
+ "Whispers",
+ "Rumors",
+ "Journeying folk",
+ "Tales"
+ ];
+ const legend = `${ra(subjects)} say a relic monster of ${length} ${heightUnit.value} long inhabits ${
+ lake.name
+ } Lake. Truth or lie, folks are afraid to fish in the lake.`;
+ notes.push({id, name, legend});
+ }
+
+ function listSeaMonsters({cells, features}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i) && features[cells.f[i]].type === "ocean"
+ );
+ }
+
+ function addSeaMonster(id, cell) {
+ const name = `${Names.getCultureShort(0)} Monster`;
+ const length = gauss(25, 10, 10, 100);
+ const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long.`;
+ notes.push({id, name, legend});
+ }
+
+ function listHillMonsters({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+ }
+
+ function addHillMonster(id, cell) {
+ const {cells} = pack;
+
+ const adjectives = [
+ "great",
+ "big",
+ "huge",
+ "prime",
+ "golden",
+ "proud",
+ "lucky",
+ "fat",
+ "giant",
+ "hungry",
+ "magical",
+ "superior",
+ "terrifying",
+ "horrifying",
+ "feared"
+ ];
+ const subjects = [
+ "Locals",
+ "Elders",
+ "Inscriptions",
+ "Tipplers",
+ "Legends",
+ "Whispers",
+ "Rumors",
+ "Journeying folk",
+ "Tales"
+ ];
+ const species = [
+ "Ogre",
+ "Troll",
+ "Cyclops",
+ "Giant",
+ "Monster",
+ "Beast",
+ "Dragon",
+ "Undead",
+ "Ghoul",
+ "Vampire",
+ "Hag",
+ "Banshee",
+ "Bearded Devil",
+ "Roc",
+ "Hydra",
+ "Warg"
+ ];
+ const modusOperandi = [
+ "steals cattle at night",
+ "prefers eating children",
+ "doesn't mind human flesh",
+ "keeps the region at bay",
+ "eats kids whole",
+ "abducts young women",
+ "terrorizes the region",
+ "harasses travelers in the area",
+ "snatches people from homes",
+ "attacks anyone who dares to approach its lair",
+ "attacks unsuspecting victims"
+ ];
+
+ const monster = ra(species);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const name = `${toponym} ${monster}`;
+ const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(
+ modusOperandi
+ )}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Sacred mountains spawn on lonely mountains
+ function listSacredMountains({cells}) {
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.h[i] >= 70 &&
+ cells.c[i].some(c => cells.culture[c]) &&
+ cells.c[i].every(c => cells.h[c] < 60)
+ );
+ }
+
+ function addSacredMountain(id, cell) {
+ const {cells, religions} = pack;
+
+ const culture = cells.c[cell].map(c => cells.culture[c]).find(c => c);
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Mountain`;
+ const height = getFriendlyHeight(cells.p[cell]);
+ const legend = `A sacred mountain of ${religions[religion].name}. Height: ${height}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Sacred forests spawn on temperate forests
+ function listSacredForests({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.culture[i] && cells.religion[i] && [6, 8].includes(cells.biome[i])
+ );
+ }
+
+ function addSacredForest(id, cell) {
+ const {cells, religions} = pack;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Forest`;
+ const legend = `A forest sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Sacred pineries spawn on boreal forests
+ function listSacredPineries({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.religion[i] && cells.biome[i] === 9);
+ }
+
+ function addSacredPinery(id, cell) {
+ const {cells, religions} = pack;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Pinery`;
+ const legend = `A pinery sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Sacred palm groves spawn on oasises
+ function listSacredPalmGroves({cells}) {
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.culture[i] &&
+ cells.religion[i] &&
+ cells.biome[i] === 1 &&
+ cells.pop[i] > 1 &&
+ Routes.isConnected(i)
+ );
+ }
+
+ function addSacredPalmGrove(id, cell) {
+ const {cells, religions} = pack;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Palm Grove`;
+ const legend = `A palm grove sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listBrigands({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && Routes.hasRoad(i));
+ }
+
+ function addBrigands(id, cell) {
+ const {cells} = pack;
+
+ const animals = [
+ "Apes",
+ "Badgers",
+ "Bears",
+ "Beavers",
+ "Bisons",
+ "Boars",
+ "Cats",
+ "Crows",
+ "Dogs",
+ "Foxes",
+ "Hares",
+ "Hawks",
+ "Hyenas",
+ "Jackals",
+ "Jaguars",
+ "Leopards",
+ "Lions",
+ "Owls",
+ "Panthers",
+ "Rats",
+ "Ravens",
+ "Rooks",
+ "Scorpions",
+ "Sharks",
+ "Snakes",
+ "Spiders",
+ "Tigers",
+ "Wolfs",
+ "Wolverines",
+ "Falcons"
+ ];
+ const types = {brigands: 4, bandits: 3, robbers: 1, highwaymen: 1};
+
+ const culture = cells.culture[cell];
+ const biome = cells.biome[cell];
+ const height = cells.p[cell];
+
+ const locality = ((height, biome) => {
+ if (height >= 70) return "highlander";
+ if ([1, 2].includes(biome)) return "desert";
+ if ([3, 4].includes(biome)) return "mounted";
+ if ([5, 6, 7, 8, 9].includes(biome)) return "forest";
+ if (biome === 12) return "swamp";
+ return "angry";
+ })(height, biome);
+
+ const name = `${Names.getCulture(culture)} ${ra(animals)}`;
+ const legend = `A gang of ${locality} ${rw(types)}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Pirates spawn on sea routes
+ function listPirates({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i));
+ }
+
+ function addPirates(id, cell) {
+ const name = "Pirates";
+ const legend = "Pirate ships have been spotted in these waters.";
+ notes.push({id, name, legend});
+ }
+
+ function listStatues({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.h[i] < 40);
+ }
+
+ function addStatue(id, cell) {
+ const {cells} = pack;
+
+ const variants = [
+ "Statue",
+ "Obelisk",
+ "Monument",
+ "Column",
+ "Monolith",
+ "Pillar",
+ "Megalith",
+ "Stele",
+ "Runestone",
+ "Sculpture",
+ "Effigy",
+ "Idol"
+ ];
+ const scripts = {
+ cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
+ geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
+ coptic: "ⲲⲴⲶⲸⲺⲼⲾⳀⳁⳂⳃⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳤ⳥⳧⳩⳪ⳫⳬⳭⳲ⳹⳾ ",
+ tibetan: "ༀ༁༂༃༄༅༆༇༈༉༊་༌༐༑༒༓༔༕༖༗༘༙༚༛༜༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿",
+ mongolian: "᠀᠐᠑᠒ᠠᠡᠦᠧᠨᠩᠪᠭᠮᠯᠰᠱᠲᠳᠵᠻᠼᠽᠾᠿᡀᡁᡆᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡙᡜᡝᡞᡟᡠᡡᡭᡮᡯᡰᡱᡲᡳᡴᢀᢁᢂᢋᢏᢐᢑᢒᢓᢛᢜᢞᢟᢠᢡᢢᢤᢥᢦ"
+ };
+
+ const culture = cells.culture[cell];
+
+ const variant = ra(variants);
+ const name = `${Names.getCulture(culture)} ${variant}`;
+ const script = scripts[ra(Object.keys(scripts))];
+ const inscription = Array(rand(40, 100))
+ .fill(null)
+ .map(() => ra(script))
+ .join("");
+ const legend = `An ancient ${variant.toLowerCase()}. It has an inscription, but no one can translate it:
+ ${inscription}
`;
+ notes.push({id, name, legend});
+ }
+
+ function listRuins({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60);
+ }
+
+ function addRuins(id, cell) {
+ const types = [
+ "City",
+ "Town",
+ "Settlement",
+ "Pyramid",
+ "Fort",
+ "Stronghold",
+ "Temple",
+ "Sacred site",
+ "Mausoleum",
+ "Outpost",
+ "Fortification",
+ "Fortress",
+ "Castle"
+ ];
+
+ const ruinType = ra(types);
+ const name = `Ruined ${ruinType}`;
+ const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
+ notes.push({id, name, legend});
+ }
+
+ function listLibraries({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.burg[i] && cells.pop[i] > 10);
+ }
+
+ function addLibrary(id, cell) {
+ const {cells} = pack;
+
+ const type = rw({Library: 3, Archive: 1, Collection: 1});
+ const name = `${Names.getCulture(cells.culture[cell])} ${type}`;
+ const legend = "A vast collection of knowledge, including many rare and ancient tomes.";
+
+ notes.push({id, name, legend});
+ }
+
+ function listCircuses({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && Routes.isConnected(i));
+ }
+
+ function addCircuse(id, cell) {
+ const adjectives = [
+ "Fantastical",
+ "Wonderous",
+ "Incomprehensible",
+ "Magical",
+ "Extraordinary",
+ "Unmissable",
+ "World-famous",
+ "Breathtaking"
+ ];
+
+ const adjective = ra(adjectives);
+ const name = `Travelling ${adjective} Circus`;
+ const legend = `Roll up, roll up, this ${adjective.toLowerCase()} circus is here for a limited time only.`;
+ notes.push({id, name, legend});
+ }
+
+ function listJousts({cells, burgs}) {
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 20);
+ }
+
+ function addJoust(id, cell) {
+ const {cells, burgs} = pack;
+ const types = ["Joust", "Competition", "Melee", "Tournament", "Contest"];
+ const virtues = ["cunning", "might", "speed", "the greats", "acumen", "brutality"];
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+ const type = ra(types);
+ const virtue = ra(virtues);
+
+ const name = `${burgName} ${type}`;
+ const legend = `Warriors from around the land gather for a ${type.toLowerCase()} of ${virtue} in ${burgName}, with fame, fortune and favour on offer to the victor.`;
+ notes.push({id, name, legend});
+ }
+
+ function listFairs({cells, burgs}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population < 20 && burgs[cells.burg[i]].population < 5
+ );
+ }
+
+ function addFair(id, cell) {
+ const {cells, burgs} = pack;
+ if (!cells.burg[cell]) return;
+
+ const burgName = burgs[cells.burg[cell]].name;
+ const type = "Fair";
+
+ const name = `${burgName} ${type}`;
+ const legend = `A fair is being held in ${burgName}, with all manner of local and foreign goods and services on offer.`;
+ notes.push({id, name, legend});
+ }
+
+ function listCanoes({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.r[i]);
+ }
+
+ function addCanoe(id, cell) {
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+
+ const name = `Minor Jetty`;
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const legend = `A small location along the ${riverName} to launch boats from sits here, along with a weary looking owner, willing to sell passage along the river.`;
+ notes.push({id, name, legend});
+ }
+
+ function listMigrations({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] <= 2);
+ }
+
+ function addMigration(id, cell) {
+ const animals = [
+ "Antelopes",
+ "Apes",
+ "Badgers",
+ "Bears",
+ "Beavers",
+ "Bisons",
+ "Boars",
+ "Buffalo",
+ "Cats",
+ "Cranes",
+ "Crocodiles",
+ "Crows",
+ "Deer",
+ "Dogs",
+ "Eagles",
+ "Elk",
+ "Foxes",
+ "Goats",
+ "Geese",
+ "Hares",
+ "Hawks",
+ "Herons",
+ "Horses",
+ "Hyenas",
+ "Ibises",
+ "Jackals",
+ "Jaguars",
+ "Larks",
+ "Leopards",
+ "Lions",
+ "Mantises",
+ "Martens",
+ "Mooses",
+ "Mules",
+ "Owls",
+ "Panthers",
+ "Rats",
+ "Ravens",
+ "Rooks",
+ "Scorpions",
+ "Sharks",
+ "Sheep",
+ "Snakes",
+ "Spiders",
+ "Tigers",
+ "Wolves",
+ "Wolverines",
+ "Camels",
+ "Falcons",
+ "Hounds",
+ "Oxen"
+ ];
+ const animalChoice = ra(animals);
+
+ const name = `${animalChoice} migration`;
+ const legend = `A huge group of ${animalChoice.toLowerCase()} are migrating, whether part of their annual routine, or something more extraordinary.`;
+ notes.push({id, name, legend});
+ }
+
+ function listDances({cells, burgs}) {
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 15);
+ }
+
+ function addDances(id, cell) {
+ const {cells, burgs} = pack;
+ const burgName = burgs[cells.burg[cell]].name;
+ const socialTypes = [
+ "gala",
+ "dance",
+ "performance",
+ "ball",
+ "soiree",
+ "jamboree",
+ "exhibition",
+ "carnival",
+ "festival",
+ "jubilee",
+ "celebration",
+ "gathering",
+ "fete"
+ ];
+ const people = [
+ "great and the good",
+ "nobility",
+ "local elders",
+ "foreign dignitaries",
+ "spiritual leaders",
+ "suspected revolutionaries"
+ ];
+ const socialType = ra(socialTypes);
+
+ const name = `${burgName} ${socialType}`;
+ const legend = `A ${socialType} has been organised at ${burgName} as a chance to gather the ${ra(
+ people
+ )} of the area together to be merry, make alliances and scheme around the crisis.`;
+ notes.push({id, name, legend});
+ }
+
+ function listMirage({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.biome[i] === 1);
+ }
+
+ function addMirage(id, cell) {
+ const adjectives = ["Entrancing", "Diaphanous", "Illusory", "Distant", "Perculiar"];
+
+ const mirageAdjective = ra(adjectives);
+ const name = `${mirageAdjective} mirage`;
+ const legend = `This ${mirageAdjective.toLowerCase()} mirage has been luring travellers out of their way for eons.`;
+ notes.push({id, name, legend});
+ }
+
+ function listCaves({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+ }
+
+ function addCave(id, cell) {
+ const {cells} = pack;
+
+ const formations = {
+ Cave: 10,
+ Cavern: 8,
+ Chasm: 6,
+ Ravine: 6,
+ Fracture: 5,
+ Grotto: 4,
+ Pit: 4,
+ Sinkhole: 2,
+ Hole: 2
+ };
+ const status = {
+ "a good spot to hid treasure": 5,
+ "the home of strange monsters": 5,
+ "totally empty": 4,
+ "endlessly deep and unexplored": 4,
+ "completely flooded": 2,
+ "slowly filling with lava": 1
+ };
+
+ let formation = rw(formations);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ if (cells.biome[cell] === 11) {
+ formation = "Glacial " + formation;
+ }
+ const name = `${toponym} ${formation}`;
+ const legend = `The ${name}. Locals claim that it is ${rw(status)}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listPortals({burgs}) {
+ return burgs
+ .slice(1, Math.ceil(burgs.length / 10) + 1)
+ .filter(({cell}) => !occupied[cell])
+ .map(burg => burg.cell);
+ }
+
+ function addPortal(id, cell) {
+ const {cells, burgs} = pack;
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+
+ const name = `${burgName} Portal`;
+ const legend = `An element of the magic portal system connecting major cities. The portals were installed centuries ago, but still work fine.`;
+ notes.push({id, name, legend});
+ }
+
+ function listRifts({cells}) {
+ return cells.i.filter(i => !occupied[i] && pack.cells.pop[i] <= 3 && biomesData.habitability[pack.cells.biome[i]]);
+ }
+
+ function addRift(id, cell) {
+ const types = ["Demonic", "Interdimensional", "Abyssal", "Cosmic", "Cataclysmic", "Subterranean", "Ancient"];
+
+ const descriptions = [
+ "all known nearby beings to flee in terror",
+ "cracks in reality itself to form",
+ "swarms of foes to spill forth",
+ "nearby plants to wither and decay",
+ "an emmissary to step through with an all-powerful relic"
+ ];
+
+ const riftType = ra(types);
+ const name = `${riftType} Rift`;
+ const legend = `A rumoured ${riftType.toLowerCase()} rift in this area is causing ${ra(descriptions)}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listDisturbedBurial({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 2);
+ }
+ function addDisturbedBurial(id, cell) {
+ const name = "Disturbed Burial";
+ const legend = "A burial site has been disturbed in this area, causing the dead to rise and attack the living.";
+ notes.push({id, name, legend});
+ }
+
+ function listNecropolis({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] < 2);
+ }
+
+ function addNecropolis(id, cell) {
+ const {cells} = pack;
+
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const type = rw({
+ Necropolis: 5,
+ Crypt: 2,
+ Tomb: 2,
+ Graveyard: 1,
+ Cemetery: 2,
+ Mausoleum: 1,
+ Sepulchre: 1
+ });
+
+ const name = `${toponym} ${type}`;
+ const legend = ra([
+ "A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls.",
+ "A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics.",
+ "This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed.",
+ "Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets.",
+ "An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers.",
+ "A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths.",
+ "This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls.",
+ "A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air.",
+ "A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners.",
+ "A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses."
+ ]);
+
+ notes.push({id, name, legend});
+ }
+
+ function listEncounters({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 1);
+ }
+
+ function addEncounter(id, cell) {
+ const name = "Random encounter";
+ const encounterSeed = cell; // use just cell Id to not overwhelm the Vercel KV database
+ const legend = `You have encountered a character.
`;
+ notes.push({id, name, legend});
+ }
+
+ return {add, generate, regenerate, getConfig, setConfig, deleteMarker};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./markers-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./markers-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in markers-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into markers-generator_render.md
diff --git a/procedural/src/engine/support/markers-generator_render.md b/procedural/src/engine/support/markers-generator_render.md
new file mode 100644
index 00000000..a1d990e1
--- /dev/null
+++ b/procedural/src/engine/support/markers-generator_render.md
@@ -0,0 +1,51 @@
+# Removed Rendering/UI Logic from markers-generator.js
+
+The following DOM manipulation and UI-related code blocks were identified and **removed** from the engine module. This logic should be moved to the Viewer/Client application:
+
+## DOM Element Manipulation
+
+### Marker Element Removal (Line 154)
+```javascript
+document.getElementById(id)?.remove();
+```
+**Purpose**: Removes marker DOM elements from the UI when regenerating markers
+**Location**: Inside the `regenerate()` function
+**Replacement**: The refactored code now returns `removedMarkerIds` array so the viewer can handle DOM cleanup
+
+### Notes Array Manipulation (Lines 155-156)
+```javascript
+const index = notes.findIndex(note => note.id === id);
+if (index != -1) notes.splice(index, 1);
+```
+**Purpose**: Removes notes from the global `notes` array when markers are deleted
+**Location**: Inside the `regenerate()` function
+**Replacement**: The engine now returns a new `notes` array instead of mutating a global one
+
+## Global State Mutations Removed
+
+### Pack Markers Direct Mutation
+```javascript
+pack.markers = [];
+pack.markers.push(marker);
+pack.markers = pack.markers.filter(...);
+```
+**Purpose**: Direct manipulation of the global `pack.markers` array
+**Replacement**: Functions now return new marker arrays instead of mutating the input
+
+### Occupied Array Global Access
+```javascript
+occupied[cell] = true;
+```
+**Purpose**: Tracking occupied cells in a module-level variable
+**Replacement**: `occupied` is now passed as a local parameter and managed within function scope
+
+## Summary
+
+The refactored engine module is now pure and stateless:
+- No DOM manipulation
+- No global state mutation
+- Returns data objects instead of side effects
+- The viewer application must handle:
+ - DOM element creation/removal based on returned marker data
+ - Note management and display
+ - State persistence and updates
\ No newline at end of file
diff --git a/procedural/src/engine/support/military-generator.js_config.md b/procedural/src/engine/support/military-generator.js_config.md
new file mode 100644
index 00000000..cd865b8e
--- /dev/null
+++ b/procedural/src/engine/support/military-generator.js_config.md
@@ -0,0 +1,23 @@
+# Config Properties for military-generator.js
+
+The refactored military-generator module requires the following configuration properties:
+
+## From options object:
+- `military` - Array of military unit configurations (defaults to getDefaultOptions() if not provided)
+- `year` - Current calendar year for note generation
+- `eraShort` - Short era designation (e.g., "AD", "BC")
+- `era` - Full era designation (e.g., "Anno Domini")
+
+## Usage:
+```javascript
+const config = {
+ military: options.military || getDefaultOptions(),
+ year: options.year,
+ eraShort: options.eraShort,
+ era: options.era
+};
+
+generate(pack, config, utils, notes);
+```
+
+**Note:** This module did not contain any direct DOM reads (byId() calls), so no additional configuration properties were needed to replace DOM access. All configuration comes from the existing global `options` object.
\ No newline at end of file
diff --git a/procedural/src/engine/support/military-generator.js_external.md b/procedural/src/engine/support/military-generator.js_external.md
new file mode 100644
index 00000000..339bfb70
--- /dev/null
+++ b/procedural/src/engine/support/military-generator.js_external.md
@@ -0,0 +1,23 @@
+# External Dependencies for military-generator.js
+
+The refactored military-generator module requires the following external dependencies to be imported:
+
+## Utility Functions
+- `d3` - D3.js library for quadtree operations and array operations (d3.sum, d3.quadtree)
+- `minmax` - Utility function to clamp values between min and max
+- `rn` - Rounding/number formatting utility function
+- `ra` - Random array element selection utility function
+- `rand` - Random number generator function
+- `gauss` - Gaussian distribution random number generator
+- `si` - SI unit formatter utility function
+- `nth` - Ordinal number formatter utility function
+
+## Runtime Configuration
+- `populationRate` - Global population rate multiplier
+- `urbanization` - Global urbanization rate
+- `TIME` - Debug timing flag
+
+## Notes System
+- `notes` - Global notes array for storing regiment notes
+
+These dependencies need to be provided via the `utils` parameter when calling the `generate` function.
\ No newline at end of file
diff --git a/procedural/src/engine/support/military-generator.js_render.md b/procedural/src/engine/support/military-generator.js_render.md
new file mode 100644
index 00000000..8fe14767
--- /dev/null
+++ b/procedural/src/engine/support/military-generator.js_render.md
@@ -0,0 +1,20 @@
+# Removed Rendering/UI Logic from military-generator.js
+
+## Analysis Result: No Rendering Logic Found
+
+After thorough analysis of the military-generator.js module, **no rendering or UI logic was identified that needed to be removed**.
+
+The module is purely computational and focuses on:
+- Calculating military units and regiments based on population, diplomacy, and geographic factors
+- Processing state-level military configurations and modifiers
+- Generating regiment data structures with composition and positioning information
+- Creating notes for regiments
+
+**No code blocks were removed** because the module does not contain:
+- DOM manipulation (no `d3.select`, `document.getElementById`, etc.)
+- SVG element creation
+- HTML content generation
+- UI event handling
+- Rendering operations
+
+The module was already well-architected as a pure data processing engine, making it suitable for headless operation without modification of its core computational logic.
\ No newline at end of file
diff --git a/procedural/src/engine/support/military-generator_prompt.md b/procedural/src/engine/support/military-generator_prompt.md
new file mode 100644
index 00000000..dcd8cb24
--- /dev/null
+++ b/procedural/src/engine/support/military-generator_prompt.md
@@ -0,0 +1,478 @@
+# military-generator.js.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `military-generator.js.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Military = (function () {
+ const generate = function () {
+ TIME && console.time("generateMilitary");
+ const {cells, states} = pack;
+ const {p} = cells;
+ const valid = states.filter(s => s.i && !s.removed); // valid states
+ if (!options.military) options.military = getDefaultOptions();
+
+ const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
+ const area = d3.sum(valid.map(s => s.area)); // total area
+ const rate = {
+ x: 0,
+ Ally: -0.2,
+ Friendly: -0.1,
+ Neutral: 0,
+ Suspicion: 0.1,
+ Enemy: 1,
+ Unknown: 0,
+ Rival: 0.5,
+ Vassal: 0.5,
+ Suzerain: -0.5
+ };
+
+ const stateModifier = {
+ melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1},
+ ranged: {Nomadic: 0.9, Highland: 1.3, Lake: 1, Naval: 0.8, Hunting: 2, River: 0.8},
+ mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
+ machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
+ naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
+ armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
+ aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
+ magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
+ };
+
+ const cellTypeModifier = {
+ nomadic: {
+ melee: 0.2,
+ ranged: 0.5,
+ mounted: 3,
+ machinery: 0.4,
+ naval: 0.3,
+ armored: 1.6,
+ aviation: 1,
+ magical: 0.5
+ },
+ wetland: {
+ melee: 0.8,
+ ranged: 2,
+ mounted: 0.3,
+ machinery: 1.2,
+ naval: 1.0,
+ armored: 0.2,
+ aviation: 0.5,
+ magical: 0.5
+ },
+ highland: {
+ melee: 1.2,
+ ranged: 1.6,
+ mounted: 0.3,
+ machinery: 3,
+ naval: 1.0,
+ armored: 0.8,
+ aviation: 0.3,
+ magical: 2
+ }
+ };
+
+ const burgTypeModifier = {
+ nomadic: {
+ melee: 0.3,
+ ranged: 0.8,
+ mounted: 3,
+ machinery: 0.4,
+ naval: 1.0,
+ armored: 1.6,
+ aviation: 1,
+ magical: 0.5
+ },
+ wetland: {
+ melee: 1,
+ ranged: 1.6,
+ mounted: 0.2,
+ machinery: 1.2,
+ naval: 1.0,
+ armored: 0.2,
+ aviation: 0.5,
+ magical: 0.5
+ },
+ highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
+ };
+
+ valid.forEach(s => {
+ s.temp = {};
+ const d = s.diplomacy;
+
+ const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
+ const diplomacyRate = d.some(d => d === "Enemy")
+ ? 1
+ : d.some(d => d === "Rival")
+ ? 0.8
+ : d.some(d => d === "Suspicion")
+ ? 0.5
+ : 0.1; // peacefulness
+ const neighborsRateRaw = s.neighbors
+ .map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion"))
+ .reduce((s, r) => (s += rate[r]), 0.5);
+ const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
+ s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
+ s.temp.platoons = [];
+
+ // apply overall state modifiers for unit types based on state features
+ for (const unit of options.military) {
+ if (!stateModifier[unit.type]) continue;
+
+ let modifier = stateModifier[unit.type][s.type] || 1;
+ if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
+ else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
+ s.temp[unit.name] = modifier * s.alert;
+ }
+ });
+
+ const getType = cell => {
+ if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
+ if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
+ if (cells.h[cell] >= 70) return "highland";
+ return "generic";
+ };
+
+ function passUnitLimits(unit, biome, state, culture, religion) {
+ if (unit.biomes && !unit.biomes.includes(biome)) return false;
+ if (unit.states && !unit.states.includes(state)) return false;
+ if (unit.cultures && !unit.cultures.includes(culture)) return false;
+ if (unit.religions && !unit.religions.includes(religion)) return false;
+ return true;
+ }
+
+ // rural cells
+ for (const i of cells.i) {
+ if (!cells.pop[i]) continue;
+
+ const biome = cells.biome[i];
+ const state = cells.state[i];
+ const culture = cells.culture[i];
+ const religion = cells.religion[i];
+
+ const stateObj = states[state];
+ if (!state || stateObj.removed) continue;
+
+ let modifier = cells.pop[i] / 100; // basic rural army in percentages
+ if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center])
+ modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
+ if (cells.f[i] !== cells.f[stateObj.center])
+ modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
+ const type = getType(i);
+
+ for (const unit of options.military) {
+ const perc = +unit.rural;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && !cells.haven[i]) continue; // only near-ocean cells create naval units
+
+ const cellTypeMod = type === "generic" ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
+ const army = modifier * perc * cellTypeMod; // rural cell army
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[i];
+ let n = 0;
+
+ // place naval units to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[i];
+ [x, y] = p[haven];
+ n = 1;
+ }
+
+ stateObj.temp.platoons.push({
+ cell: i,
+ a: total,
+ t: total,
+ x,
+ y,
+ u: unit.name,
+ n,
+ s: unit.separate,
+ type: unit.type
+ });
+ }
+ }
+
+ // burgs
+ for (const b of pack.burgs) {
+ if (!b.i || b.removed || !b.state || !b.population) continue;
+
+ const biome = cells.biome[b.cell];
+ const state = b.state;
+ const culture = b.culture;
+ const religion = cells.religion[b.cell];
+
+ const stateObj = states[state];
+ let m = (b.population * urbanization) / 100; // basic urban army in percentages
+ if (b.capital) m *= 1.2; // capital has household troops
+ if (culture !== stateObj.culture) m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center]) m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
+ if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
+ const type = getType(b.cell);
+
+ for (const unit of options.military) {
+ const perc = +unit.urban;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && (!b.port || !cells.haven[b.cell])) continue; // only ports create naval units
+
+ const mod = type === "generic" ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
+ const army = m * perc * mod; // urban cell army
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[b.cell];
+ let n = 0;
+
+ // place naval to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[b.cell];
+ [x, y] = p[haven];
+ n = 1;
+ }
+
+ stateObj.temp.platoons.push({
+ cell: b.cell,
+ a: total,
+ t: total,
+ x,
+ y,
+ u: unit.name,
+ n,
+ s: unit.separate,
+ type: unit.type
+ });
+ }
+ }
+
+ const expected = 3 * populationRate; // expected regiment size
+ const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
+
+ // get regiments for each state
+ valid.forEach(s => {
+ s.military = createRegiments(s.temp.platoons, s);
+ delete s.temp; // do not store temp data
+ });
+
+ function createRegiments(nodes, s) {
+ if (!nodes.length) return [];
+
+ nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
+ const tree = d3.quadtree(
+ nodes,
+ d => d.x,
+ d => d.y
+ );
+
+ nodes.forEach(node => {
+ tree.remove(node);
+ const overlap = tree.find(node.x, node.y, 20);
+ if (overlap && overlap.t && mergeable(node, overlap)) {
+ merge(node, overlap);
+ return;
+ }
+ if (node.t > expected) return;
+ const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
+ const candidates = tree.findAll(node.x, node.y, r);
+ for (const c of candidates) {
+ if (c.t < expected && mergeable(node, c)) {
+ merge(node, c);
+ break;
+ }
+ }
+ });
+
+ // add n0 to n1's ultimate parent
+ function merge(n0, n1) {
+ if (!n1.childen) n1.childen = [n0];
+ else n1.childen.push(n0);
+ if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
+ n1.t += n0.t;
+ n0.t = 0;
+ }
+
+ // parse regiments data
+ const regiments = nodes
+ .filter(n => n.t)
+ .sort((a, b) => b.t - a.t)
+ .map((r, i) => {
+ const u = {};
+ u[r.u] = r.a;
+ (r.childen || []).forEach(n => (u[n.u] = u[n.u] ? (u[n.u] += n.a) : n.a));
+ return {i, a: r.t, cell: r.cell, x: r.x, y: r.y, bx: r.x, by: r.y, u, n: r.n, name, state: s.i};
+ });
+
+ // generate name for regiments
+ regiments.forEach(r => {
+ r.name = getName(r, regiments);
+ r.icon = getEmblem(r);
+ generateNote(r, s);
+ });
+
+ return regiments;
+ }
+
+ TIME && console.timeEnd("generateMilitary");
+ };
+
+ const getDefaultOptions = function () {
+ return [
+ {icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
+ {icon: "🏹", name: "archers", rural: 0.12, urban: 0.2, crew: 1, power: 1, type: "ranged", separate: 0},
+ {icon: "🐴", name: "cavalry", rural: 0.12, urban: 0.03, crew: 2, power: 2, type: "mounted", separate: 0},
+ {icon: "💣", name: "artillery", rural: 0, urban: 0.03, crew: 8, power: 12, type: "machinery", separate: 0},
+ {icon: "🌊", name: "fleet", rural: 0, urban: 0.015, crew: 100, power: 50, type: "naval", separate: 1}
+ ];
+ };
+
+ // utilize si function to make regiment total text fit regiment box
+ const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
+
+ const getName = function (r, regiments) {
+ const cells = pack.cells;
+ const proper = r.n
+ ? null
+ : cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
+ ? pack.provinces[cells.province[r.cell]].name
+ : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
+ ? pack.burgs[cells.burg[r.cell]].name
+ : null;
+ const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
+ const form = r.n ? "Fleet" : "Regiment";
+ return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
+ };
+
+ // get default regiment emblem
+ const getEmblem = function (r) {
+ if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
+ if (
+ !r.n &&
+ pack.states[r.state].form === "Monarchy" &&
+ pack.cells.burg[r.cell] &&
+ pack.burgs[pack.cells.burg[r.cell]].capital
+ )
+ return "👑"; // "Royal" regiment based in capital
+ const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
+ const unit = options.military.find(u => u.name === mainUnit);
+ return unit.icon;
+ };
+
+ const generateNote = function (r, s) {
+ const cells = pack.cells;
+ const base =
+ cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
+ ? pack.burgs[cells.burg[r.cell]].name
+ : cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
+ ? pack.provinces[cells.province[r.cell]].fullName
+ : null;
+ const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
+
+ const composition = r.a
+ ? Object.keys(r.u)
+ .map(t => `— ${t}: ${r.u[t]}`)
+ .join("\r\n")
+ : null;
+ const troops = composition
+ ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.`
+ : "";
+
+ const campaign = s.campaigns ? ra(s.campaigns) : null;
+ const year = campaign
+ ? rand(campaign.start, campaign.end || options.year)
+ : gauss(options.year - 100, 150, 1, options.year - 6);
+ const conflict = campaign ? ` during the ${campaign.name}` : "";
+ const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
+ notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
+ };
+
+ return {
+ generate,
+ getDefaultOptions,
+ getName,
+ generateNote,
+ getTotal,
+ getEmblem
+ };
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./military-generator.js.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./military-generator.js_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in military-generator.js_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into military-generator.js_render.md
diff --git a/procedural/src/engine/support/names-generator_config.md b/procedural/src/engine/support/names-generator_config.md
new file mode 100644
index 00000000..46c5bfa2
--- /dev/null
+++ b/procedural/src/engine/support/names-generator_config.md
@@ -0,0 +1,25 @@
+# Config Properties for names-generator.js
+
+The refactored `names-generator.js` module identified the following configuration properties that were previously read from the DOM:
+
+## Configuration Properties
+
+Currently, no direct config properties were identified in this module, as the original code had minimal DOM interaction. The main DOM interaction was:
+
+- **Map Name Storage**: The original code wrote to `mapName.value` but this was rendering logic that has been removed.
+
+## Notes
+
+- The `getMapName()` function previously wrote directly to a DOM element (`mapName.value = name`)
+- This has been removed and the function now returns the generated name instead
+- The calling code (Viewer/Client) should handle storing or displaying the generated map name
+- All name generation functions now operate purely on the data passed to them as parameters
+
+## Function Signature Changes
+
+Functions that previously read global state now require data to be passed as parameters:
+
+- `getCulture()` now requires `cultures` parameter
+- `getCultureShort()` now requires `cultures` parameter
+- `getState()` now requires `cultures` parameter
+- All functions now require `nameBases` and `utils` parameters
\ No newline at end of file
diff --git a/procedural/src/engine/support/names-generator_external.md b/procedural/src/engine/support/names-generator_external.md
new file mode 100644
index 00000000..096d0434
--- /dev/null
+++ b/procedural/src/engine/support/names-generator_external.md
@@ -0,0 +1,24 @@
+# External Dependencies for names-generator.js
+
+The refactored `names-generator.js` module requires the following external dependencies to be imported:
+
+## Utility Functions
+- `ERROR` - Error logging flag/function
+- `WARN` - Warning logging flag/function
+- `P` - Probability function (returns true/false based on probability)
+- `ra` - Random array element selector function
+- `last` - Function to get last character/element of a string/array
+- `vowel` - Function to check if a character is a vowel
+- `capitalize` - Function to capitalize a string
+- `rand` - Random number generator function
+
+These utilities should be imported from a common utilities module (e.g., `../utils/index.js`) and passed as a `utils` object parameter to the exported functions.
+
+## Data Dependencies
+- `nameBases` - Array of name base configurations (passed as parameter)
+- `cultures` - Culture data object with base references (passed as parameter from pack data)
+
+## Notes
+- All global state access has been removed and replaced with parameter injection
+- The module is now pure and environment-agnostic
+- No browser or DOM dependencies remain
\ No newline at end of file
diff --git a/procedural/src/engine/support/names-generator_prompt.md b/procedural/src/engine/support/names-generator_prompt.md
new file mode 100644
index 00000000..5e60358a
--- /dev/null
+++ b/procedural/src/engine/support/names-generator_prompt.md
@@ -0,0 +1,371 @@
+# names-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `names-generator.js`.
+
+**File Content:**
+```javascript
+"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"},
+ // 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
+ };
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./names-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./names-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in names-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into names-generator_render.md
diff --git a/procedural/src/engine/support/names-generator_render.md b/procedural/src/engine/support/names-generator_render.md
new file mode 100644
index 00000000..c2f3d4a8
--- /dev/null
+++ b/procedural/src/engine/support/names-generator_render.md
@@ -0,0 +1,46 @@
+# Removed Rendering/UI Logic from names-generator.js
+
+The following rendering and UI logic was removed from the `names-generator.js` module and should be implemented in the Viewer/Client application:
+
+## DOM Manipulation
+
+### Map Name Storage
+**Original Code (lines 325):**
+```javascript
+mapName.value = name;
+```
+
+**Location**: `getMapName()` function
+**Description**: Direct DOM manipulation to set the value of a map name input field
+**Replacement**: The `getMapName()` function now returns the generated name instead of setting it directly
+
+## UI Feedback and State Management
+
+### Lock State Checks
+**Original Code (lines 314-315):**
+```javascript
+if (!force && locked("mapName")) return;
+if (force && locked("mapName")) unlock("mapName");
+```
+
+**Location**: `getMapName()` function
+**Description**: UI state management for locking/unlocking map name generation
+**Replacement**: These checks should be handled by the Viewer/Client before calling `getMapName()`
+
+### User Notifications
+**Original Code (lines 149, 318):**
+```javascript
+tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
+tip("Namebase is not found", false, "error");
+```
+
+**Location**: `getBase()` and `getMapName()` functions
+**Description**: UI notifications/tooltips to inform user of errors
+**Replacement**: Error handling should be done by the Viewer/Client based on return values or thrown errors
+
+## Implementation Notes for Viewer/Client
+
+1. **Map Name Generation**: Call `getMapName()` and handle the returned value by setting it to the appropriate DOM element
+2. **Lock State Management**: Implement lock/unlock logic in the UI layer before calling name generation functions
+3. **Error Display**: Handle error states and display appropriate user feedback when name generation fails
+4. **State Persistence**: Handle saving/loading of generated names as needed by the application
\ No newline at end of file
diff --git a/procedural/src/engine/support/ocean-layers-original.txt b/procedural/src/engine/support/ocean-layers-original.txt
new file mode 100644
index 00000000..281fad0a
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers-original.txt
@@ -0,0 +1,92 @@
+"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;
+})();
diff --git a/procedural/src/engine/support/ocean-layers-refactor.txt b/procedural/src/engine/support/ocean-layers-refactor.txt
new file mode 100644
index 00000000..cc9a8fb5
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers-refactor.txt
@@ -0,0 +1,94 @@
+"use strict";
+
+export function generateOceanLayers(grid, config, utils) {
+ const { lineGen, clipPoly, round, rn, P } = utils;
+
+ if (config.outline === "none") return { layers: [] };
+
+ const cells = grid.cells;
+ const pointsN = grid.cells.i.length;
+ const vertices = grid.vertices;
+ const limits = config.outline === "random" ? randomizeOutline(P) : config.outline.split(",").map(s => +s);
+
+ const chains = [];
+ const opacity = rn(0.4 / limits.length, 2);
+ const 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, cells, vertices, pointsN);
+ if (!start) continue;
+ used[i] = 1;
+ const chain = connectVertices(start, t, cells, vertices, pointsN, used); // 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]);
+ }
+
+ const layers = [];
+ for (const t of limits) {
+ const layer = chains.filter(c => c[0] === t);
+ const paths = layer.map(c => round(lineGen(c[1]))).filter(path => path);
+ if (paths.length > 0) {
+ layers.push({
+ type: t,
+ paths: paths,
+ opacity: opacity
+ });
+ }
+ }
+
+ return { layers };
+}
+
+function randomizeOutline(P) {
+ 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;
+}
+
+// find eligible cell vertex to start path detection
+function findStart(i, t, cells, vertices, pointsN) {
+ 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])];
+}
+
+// connect vertices to chain
+function connectVertices(start, t, cells, vertices, pointsN, used) {
+ 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]) {
+ console.error("Next vertex is not found");
+ break;
+ }
+ }
+ chain.push(chain[0]); // push first vertex as the last one
+ return chain;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/ocean-layers_config.md b/procedural/src/engine/support/ocean-layers_config.md
new file mode 100644
index 00000000..edc76e5d
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers_config.md
@@ -0,0 +1,27 @@
+# Config Properties for ocean-layers.js
+
+The refactored `ocean-layers.js` module requires the following configuration properties:
+
+## Required Config Properties
+
+### `outline`
+- **Type**: `string`
+- **Description**: Defines the ocean layer outline configuration
+- **Values**:
+ - `"none"` - No ocean layers will be generated
+ - `"random"` - Use randomized outline limits
+ - Comma-separated numbers (e.g., `"-1,-2,-3"`) - Specific depth limits for layer generation
+- **Original DOM Source**: `oceanLayers.attr("layers")`
+- **Usage**: Controls which ocean depth levels should have visible layers drawn
+
+## Example Config Object
+
+```javascript
+const config = {
+ outline: "-1,-2,-3" // Generate layers for depths -1, -2, and -3
+};
+```
+
+## Migration Notes
+
+The original code read this value directly from a DOM element's `layers` attribute. In the refactored version, this configuration must be provided via the `config` parameter to maintain environment independence.
\ No newline at end of file
diff --git a/procedural/src/engine/support/ocean-layers_external.md b/procedural/src/engine/support/ocean-layers_external.md
new file mode 100644
index 00000000..ec85083d
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers_external.md
@@ -0,0 +1,17 @@
+# External Dependencies for ocean-layers.js
+
+The refactored `ocean-layers.js` module requires the following external utilities to be imported:
+
+## Required Utilities (from utils object)
+
+- `lineGen` - D3 line generator for creating SVG path strings from point arrays
+- `clipPoly` - Function to clip polygons, likely for map boundary handling
+- `round` - Rounding utility function for numeric precision
+- `rn` - Random number utility function
+- `P` - Probability utility function for random boolean generation
+
+These utilities should be passed in via the `utils` parameter when calling `generateOceanLayers()`.
+
+## Note on D3 Dependency
+
+The `lineGen` utility appears to be a D3.js line generator that was previously accessed globally. The engine module now receives this as a dependency, maintaining separation from browser-specific D3 imports.
\ No newline at end of file
diff --git a/procedural/src/engine/support/ocean-layers_prompt.md b/procedural/src/engine/support/ocean-layers_prompt.md
new file mode 100644
index 00000000..68b476be
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers_prompt.md
@@ -0,0 +1,176 @@
+# ocean-layers.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `ocean-layers.js`.
+
+**File Content:**
+```javascript
+"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;
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./ocean-layers.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./ocean-layers_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in ocean-layers_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into ocean-layers_render.md
diff --git a/procedural/src/engine/support/ocean-layers_render.md b/procedural/src/engine/support/ocean-layers_render.md
new file mode 100644
index 00000000..28ef02b3
--- /dev/null
+++ b/procedural/src/engine/support/ocean-layers_render.md
@@ -0,0 +1,55 @@
+# Removed Rendering Logic from ocean-layers.js
+
+The following DOM manipulation and SVG rendering code blocks were removed from the engine module and should be implemented in the Viewer application:
+
+## Removed DOM/SVG Rendering Code
+
+### 1. DOM Configuration Reading
+**Original Code (Line 79):**
+```javascript
+const outline = oceanLayers.attr("layers");
+```
+**Reason for Removal**: Direct DOM element access for reading configuration
+
+### 2. SVG Path Creation and Styling
+**Original Code (Line 113):**
+```javascript
+if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity);
+```
+**Reason for Removal**: Direct SVG DOM manipulation for rendering ocean layer paths
+
+### 3. Performance Timing (Debug)
+**Original Code (Lines 81, 122):**
+```javascript
+TIME && console.time("drawOceanLayers");
+// ... at end of function ...
+TIME && console.timeEnd("drawOceanLayers");
+```
+**Reason for Removal**: Debug/timing logic should be handled by the viewer application
+
+## Viewer Implementation Guidance
+
+The Viewer application should:
+
+1. **Configuration**: Read the `layers` attribute from the `oceanLayers` DOM element and pass it as `config.outline`
+2. **Rendering**: Take the returned `layers` array and create SVG `` elements with:
+ - `d` attribute set to each path string
+ - `fill` attribute set to `#ecf2f9`
+ - `fill-opacity` attribute set to the calculated opacity value
+3. **Performance**: Optionally implement timing logic using `console.time/timeEnd` if needed
+
+## Data Structure Returned by Engine
+
+The engine now returns a structured object instead of directly manipulating the DOM:
+
+```javascript
+{
+ layers: [
+ {
+ type: -1, // depth level
+ paths: ["M10,20L30,40..."], // array of SVG path strings
+ opacity: 0.13 // calculated opacity value
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/original_resample.txt b/procedural/src/engine/support/original_resample.txt
new file mode 100644
index 00000000..21349b9e
--- /dev/null
+++ b/procedural/src/engine/support/original_resample.txt
@@ -0,0 +1,367 @@
+"use strict";
+
+window.Resample = (function () {
+ /*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+ */
+ function process({projection, inverse, scale}) {
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(pack.rivers);
+
+ grid = generateGrid();
+ pack = {};
+ notes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale);
+
+ Features.markupGrid();
+ addLakesInDeepDepressions();
+ openNearSeaLakes();
+
+ OceanLayers();
+ calculateMapCoordinates();
+ calculateTemperatures();
+
+ reGraph();
+ Features.markupPack();
+ createDefaultRuler();
+
+ restoreCellData(parentMap, inverse, scale);
+ restoreRivers(riversData, projection, scale);
+ restoreCultures(parentMap, projection);
+ restoreBurgs(parentMap, projection, scale);
+ restoreStates(parentMap, projection);
+ restoreRoutes(parentMap, projection);
+ restoreReligions(parentMap, projection);
+ restoreProvinces(parentMap);
+ restoreFeatureDetails(parentMap, inverse);
+ restoreMarkers(parentMap, projection);
+ restoreZones(parentMap, projection, scale);
+
+ showStatistics();
+ }
+
+ function resamplePrimaryGridData(parentMap, inverse, scale) {
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap();
+ }
+
+ function smoothHeightmap() {
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+ }
+
+ function restoreCellData(parentMap, inverse, scale) {
+ pack.cells.biome = new Uint8Array(pack.cells.i.length);
+ pack.cells.fl = new Uint16Array(pack.cells.i.length);
+ pack.cells.s = new Int16Array(pack.cells.i.length);
+ pack.cells.pop = new Float32Array(pack.cells.i.length);
+ pack.cells.culture = new Uint16Array(pack.cells.i.length);
+ pack.cells.state = new Uint16Array(pack.cells.i.length);
+ pack.cells.burg = new Uint16Array(pack.cells.i.length);
+ pack.cells.religion = new Uint16Array(pack.cells.i.length);
+ pack.cells.province = new Uint16Array(pack.cells.i.length);
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+ }
+
+ function saveRiversData(parentRivers) {
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+ }
+
+ function restoreRivers(riversData, projection, scale) {
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ pack.rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ pack.rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+ }
+
+ function restoreCultures(parentMap, projection) {
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+ pack.cultures = parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+ }
+
+ function restoreBurgs(parentMap, projection, scale) {
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ pack.burgs = parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+ }
+
+ function restoreStates(parentMap, projection) {
+ const validStates = new Set(pack.cells.state);
+ pack.states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ pack.states = pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+ }
+
+ function restoreRoutes(parentMap, projection) {
+ pack.routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, graphWidth, graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(pack.routes);
+ }
+
+ function restoreReligions(parentMap, projection) {
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ pack.religions = parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+ }
+
+ function restoreProvinces(parentMap) {
+ const validProvinces = new Set(pack.cells.province);
+ pack.provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+ }
+
+ function restoreMarkers(parentMap, projection) {
+ pack.markers = parentMap.pack.markers;
+ pack.markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+ }
+
+ function restoreZones(parentMap, projection, scale) {
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ pack.zones = parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+ }
+
+ function restoreFeatureDetails(parentMap, inverse) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+ }
+
+ function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+ }
+
+ function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+ }
+
+ function isInMap(x, y) {
+ return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
+ }
+
+ return {process};
+})();
diff --git a/procedural/src/engine/support/provinces-generator.js_config.md b/procedural/src/engine/support/provinces-generator.js_config.md
new file mode 100644
index 00000000..d87b79f5
--- /dev/null
+++ b/procedural/src/engine/support/provinces-generator.js_config.md
@@ -0,0 +1,25 @@
+# Config Properties for provinces-generator.js
+
+The refactored `provinces-generator.js` module requires the following configuration properties:
+
+## Required Config Properties:
+
+### `provincesRatio` (Number)
+- **Original DOM source**: `byId("provincesRatio").value`
+- **Description**: Ratio determining the number of provinces to generate (0-100)
+- **Usage**: Controls how many provinces are created relative to the number of burgs in each state
+- **Type**: Number (typically 0-100)
+
+### `seed` (String/Number)
+- **Description**: Random seed for province generation
+- **Usage**: Used when `regenerate` parameter is false to maintain consistent generation
+- **Type**: String or Number
+- **Note**: This replaces the global `seed` variable access
+
+## Config Object Structure:
+```javascript
+const config = {
+ provincesRatio: 50, // 0-100, percentage of burgs to become province centers
+ seed: "some-seed-value" // Random seed for reproducible generation
+};
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/provinces-generator.js_external.md b/procedural/src/engine/support/provinces-generator.js_external.md
new file mode 100644
index 00000000..1e8be9cd
--- /dev/null
+++ b/procedural/src/engine/support/provinces-generator.js_external.md
@@ -0,0 +1,33 @@
+# External Dependencies for provinces-generator.js
+
+The refactored `provinces-generator.js` module requires the following external modules and utilities to be imported:
+
+## Core Utilities (passed via `utils` object):
+- `TIME` - Debug timing flag
+- `generateSeed()` - Random seed generation function
+- `aleaPRNG()` - Seeded pseudo-random number generator
+- `gauss()` - Gaussian distribution function
+- `P()` - Probability function (random true/false with given probability)
+- `Names` - Name generation utilities
+ - `Names.getState()`
+ - `Names.getCultureShort()`
+- `rw()` - Weighted random selection function
+- `getMixedColor()` - Color mixing utility
+- `BurgsAndStates` - Burg and state utilities
+ - `BurgsAndStates.getType()`
+- `COA` - Coat of Arms generation utilities
+ - `COA.generate()`
+ - `COA.getShield()`
+- `FlatQueue` - Priority queue implementation
+- `d3` - D3.js library (specifically `d3.max()`)
+- `rand()` - Random integer generation function
+- `getPolesOfInaccessibility()` - Pole of inaccessibility calculation function
+
+## Data Dependencies (passed as parameters):
+- `pack` - Main data structure containing:
+ - `pack.cells` - Cell data arrays
+ - `pack.states` - State definitions
+ - `pack.burgs` - Settlement data
+ - `pack.provinces` - Existing province data (for regeneration)
+ - `pack.features` - Geographic feature data
+- `config` - Configuration object (see config file for details)
\ No newline at end of file
diff --git a/procedural/src/engine/support/provinces-generator.js_render.md b/procedural/src/engine/support/provinces-generator.js_render.md
new file mode 100644
index 00000000..6eb3b424
--- /dev/null
+++ b/procedural/src/engine/support/provinces-generator.js_render.md
@@ -0,0 +1,23 @@
+# Removed Rendering/UI Logic from provinces-generator.js
+
+## Analysis Result:
+
+**No rendering or UI logic was found in the original `provinces-generator.js.js` file.**
+
+The original module was purely computational and contained:
+- Province generation algorithms
+- Geographic calculations
+- Data structure manipulations
+- Mathematical computations for province boundaries
+
+## What was NOT present (and therefore not removed):
+- No DOM manipulation code
+- No SVG rendering logic
+- No `d3.select()` calls for visualization
+- No `document.getElementById()` calls for DOM updates
+- No HTML element creation or modification
+- No CSS styling operations
+- No canvas or WebGL rendering code
+
+## Note:
+This module was already well-separated in terms of concerns - it focused purely on the computational aspects of province generation without any visualization or user interface components. The only UI-related code was the single DOM read (`byId("provincesRatio").value`) which has been converted to a config property (`config.provincesRatio`).
\ No newline at end of file
diff --git a/procedural/src/engine/support/provinces-generator_prompt.md b/procedural/src/engine/support/provinces-generator_prompt.md
new file mode 100644
index 00000000..9d7f68fe
--- /dev/null
+++ b/procedural/src/engine/support/provinces-generator_prompt.md
@@ -0,0 +1,341 @@
+# provinces-generator.js.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `provinces-generator.js.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Provinces = (function () {
+ const forms = {
+ Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
+ Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
+ Theocracy: {Parish: 3, Deanery: 1},
+ Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
+ Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
+ Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
+ };
+
+ const generate = (regenerate = false, regenerateLockedStates = false) => {
+ TIME && console.time("generateProvinces");
+ const localSeed = regenerate ? generateSeed() : seed;
+ Math.random = aleaPRNG(localSeed);
+
+ const {cells, states, burgs} = pack;
+ const provinces = [0]; // 0 index is reserved for "no province"
+ const provinceIds = new Uint16Array(cells.i.length);
+
+ const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
+ const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
+
+ if (regenerate) {
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed || !isProvinceLocked(province)) return;
+
+ const newId = provinces.length;
+ for (const i of cells.i) {
+ if (cells.province[i] === province.i) provinceIds[i] = newId;
+ }
+
+ province.i = newId;
+ provinces.push(province);
+ });
+ }
+
+ const provincesRatio = +byId("provincesRatio").value;
+ const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
+
+ // generate provinces for selected burgs
+ states.forEach(s => {
+ s.provinces = [];
+ if (!s.i || s.removed) return;
+ if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
+ if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
+
+ const stateBurgs = burgs
+ .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
+ .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
+ .sort((a, b) => b.capital - a.capital);
+ if (stateBurgs.length < 2) return; // at least 2 provinces are required
+ const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
+
+ const form = Object.assign({}, forms[s.form]);
+
+ for (let i = 0; i < provincesNumber; i++) {
+ const provinceId = provinces.length;
+ const center = stateBurgs[i].cell;
+ const burg = stateBurgs[i].i;
+ const c = stateBurgs[i].culture;
+ const nameByBurg = P(0.5);
+ const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
+ const formName = rw(form);
+ form[formName] += 10;
+ const fullName = name + " " + formName;
+ const color = getMixedColor(s.color);
+ const kinship = nameByBurg ? 0.8 : 0.4;
+ const type = BurgsAndStates.getType(center, burg.port);
+ const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ s.provinces.push(provinceId);
+ provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
+ }
+ });
+
+ // expand generated provinces
+ const queue = new FlatQueue();
+ const cost = [];
+
+ provinces.forEach(p => {
+ if (!p.i || p.removed || isProvinceLocked(p)) return;
+ provinceIds[p.center] = p.i;
+ queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
+ cost[p.center] = 1;
+ });
+
+ while (queue.length) {
+ const {e, p, province, state} = queue.pop();
+
+ cells.c[e].forEach(e => {
+ if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
+
+ const land = cells.h[e] >= 20;
+ if (!land && !cells.t[e]) return; // cannot pass deep ocean
+ if (land && cells.state[e] !== state) return;
+ const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
+ const totalCost = p + evevation;
+
+ if (totalCost > max) return;
+ if (!cost[e] || totalCost < cost[e]) {
+ if (land) provinceIds[e] = province; // assign province to a cell
+ cost[e] = totalCost;
+ queue.push({e, province, state, p: totalCost}, totalCost);
+ }
+ });
+ }
+
+ // justify provinces shapes a bit
+ for (const i of cells.i) {
+ if (cells.burg[i]) continue; // do not overwrite burgs
+ if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
+
+ const neibs = cells.c[i]
+ .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
+ .map(c => provinceIds[c]);
+ const adversaries = neibs.filter(c => c !== provinceIds[i]);
+ if (adversaries.length < 2) continue;
+
+ const buddies = neibs.filter(c => c === provinceIds[i]).length;
+ if (buddies.length > 2) continue;
+
+ const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
+ const max = d3.max(competitors);
+ if (buddies >= max) continue;
+
+ provinceIds[i] = adversaries[competitors.indexOf(max)];
+ }
+
+ // add "wild" provinces if some cells don't have a province assigned
+ const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
+ states.forEach(s => {
+ if (!s.i || s.removed) return;
+ if (s.lock && !regenerateLockedStates) return;
+ if (!s.provinces.length) return;
+
+ const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
+ const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
+ const getColonyName = () => {
+ if (colonyNamePool.length < 1) return null;
+
+ const index = rand(colonyNamePool.length - 1);
+ const spliced = colonyNamePool.splice(index, 1);
+ return spliced[0] ? `New ${spliced[0]}` : null;
+ };
+
+ let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ while (stateNoProvince.length) {
+ // add new province
+ const provinceId = provinces.length;
+ const burgCell = stateNoProvince.find(i => cells.burg[i]);
+ const center = burgCell ? burgCell : stateNoProvince[0];
+ const burg = burgCell ? cells.burg[burgCell] : 0;
+ provinceIds[center] = provinceId;
+
+ // expand province
+ const cost = [];
+ cost[center] = 1;
+ queue.push({e: center, p: 0}, 0);
+ while (queue.length) {
+ const {e, p} = queue.pop();
+
+ cells.c[e].forEach(nextCellId => {
+ if (provinceIds[nextCellId]) return;
+ const land = cells.h[nextCellId] >= 20;
+ if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
+ const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
+ const totalCost = p + ter;
+
+ if (totalCost > max) return;
+ if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
+ if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
+ cost[nextCellId] = totalCost;
+ queue.push({e: nextCellId, p: totalCost}, totalCost);
+ }
+ });
+ }
+
+ // generate "wild" province name
+ const c = cells.culture[center];
+ const f = pack.features[cells.f[center]];
+ const color = getMixedColor(s.color);
+
+ const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
+ const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
+ const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
+ const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
+
+ const name = (() => {
+ const colonyName = colony && P(0.8) && getColonyName();
+ if (colonyName) return colonyName;
+ if (burgCell && P(0.5)) return burgs[burg].name;
+ return Names.getState(Names.getCultureShort(c), c);
+ })();
+
+ const formName = (() => {
+ if (singleIsle) return "Island";
+ if (isleGroup) return "Islands";
+ if (colony) return "Colony";
+ return rw(forms["Wild"]);
+ })();
+
+ const fullName = name + " " + formName;
+
+ const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
+ const kinship = dominion ? 0 : 0.4;
+ const type = BurgsAndStates.getType(center, burgs[burg]?.port);
+ const coa = COA.generate(s.coa, kinship, dominion, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
+ s.provinces.push(provinceId);
+
+ // check if there is a land way within the same state between two cells
+ function isPassable(from, to) {
+ if (cells.f[from] !== cells.f[to]) return false; // on different islands
+ const passableQueue = [from],
+ used = new Uint8Array(cells.i.length),
+ state = cells.state[from];
+ while (passableQueue.length) {
+ const current = passableQueue.pop();
+ if (current === to) return true; // way is found
+ cells.c[current].forEach(c => {
+ if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
+ passableQueue.push(c);
+ used[c] = 1;
+ });
+ }
+ return false; // way is not found
+ }
+
+ // re-check
+ stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ }
+ });
+
+ cells.province = provinceIds;
+ pack.provinces = provinces;
+
+ TIME && console.timeEnd("generateProvinces");
+ };
+
+ // calculate pole of inaccessibility for each province
+ const getPoles = () => {
+ const getType = cellId => pack.cells.province[cellId];
+ const poles = getPolesOfInaccessibility(pack, getType);
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ province.pole = poles[province.i] || [0, 0];
+ });
+ };
+
+ return {generate, getPoles};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./provinces-generator.js.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./provinces-generator.js_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in provinces-generator.js_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into provinces-generator.js_render.md
diff --git a/procedural/src/engine/support/refactored_resample.txt b/procedural/src/engine/support/refactored_resample.txt
new file mode 100644
index 00000000..e0db3189
--- /dev/null
+++ b/procedural/src/engine/support/refactored_resample.txt
@@ -0,0 +1,410 @@
+"use strict";
+
+/*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+*/
+export function process({projection, inverse, scale}, grid, pack, notes, config, utils) {
+ const {deepCopy, generateGrid, rn, findCell, findAll, isInMap, unique, lineclip, WARN} = utils;
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(parentMap.pack.rivers, utils);
+
+ const newGrid = generateGrid();
+ const newPack = {};
+ const newNotes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale, newGrid, utils);
+
+ // External module calls that modify newGrid and newPack would need to be handled by caller
+ // Features.markupGrid(), addLakesInDeepDepressions(), openNearSeaLakes(),
+ // OceanLayers(), calculateMapCoordinates(), calculateTemperatures(),
+ // reGraph(), Features.markupPack(), createDefaultRuler()
+
+ const cellData = restoreCellData(parentMap, inverse, scale, newPack, config, utils);
+ const rivers = restoreRivers(riversData, projection, scale, newPack, config, utils);
+ const cultures = restoreCultures(parentMap, projection, newPack, utils);
+ const burgs = restoreBurgs(parentMap, projection, scale, newPack, utils);
+ const states = restoreStates(parentMap, projection, newPack, config, utils);
+ const routes = restoreRoutes(parentMap, projection, newPack, config, utils);
+ const religions = restoreReligions(parentMap, projection, newPack, utils);
+ const provinces = restoreProvinces(parentMap, newPack, utils);
+ const featureDetails = restoreFeatureDetails(parentMap, inverse, newPack, utils);
+ const markers = restoreMarkers(parentMap, projection, newPack, utils);
+ const zones = restoreZones(parentMap, projection, scale, newPack, utils);
+
+ return {
+ grid: newGrid,
+ pack: {
+ ...newPack,
+ cells: cellData.cells,
+ rivers: rivers,
+ cultures: cultures,
+ burgs: burgs,
+ states: states,
+ routes: routes,
+ religions: religions,
+ provinces: provinces,
+ markers: markers,
+ zones: zones,
+ features: newPack.features || []
+ },
+ notes: newNotes
+ };
+}
+
+function resamplePrimaryGridData(parentMap, inverse, scale, grid, utils) {
+ const {smoothHeightmap} = utils;
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap(grid);
+}
+
+function smoothHeightmap(grid) {
+ const {d3, isWater} = grid.utils || {};
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+}
+
+function restoreCellData(parentMap, inverse, scale, pack, config, utils) {
+ const {d3, isWater} = utils;
+
+ const cells = {
+ biome: new Uint8Array(pack.cells.i.length),
+ fl: new Uint16Array(pack.cells.i.length),
+ s: new Int16Array(pack.cells.i.length),
+ pop: new Float32Array(pack.cells.i.length),
+ culture: new Uint16Array(pack.cells.i.length),
+ state: new Uint16Array(pack.cells.i.length),
+ burg: new Uint16Array(pack.cells.i.length),
+ religion: new Uint16Array(pack.cells.i.length),
+ province: new Uint16Array(pack.cells.i.length)
+ };
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+
+ return {cells};
+}
+
+function saveRiversData(parentRivers, utils) {
+ const {Rivers} = utils;
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+}
+
+function restoreRivers(riversData, projection, scale, pack, config, utils) {
+ const {rn, isInMap, findCell, Rivers} = utils;
+
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ const rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config.graphWidth, config.graphHeight);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+
+ return rivers;
+}
+
+function restoreCultures(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, getPolesOfInaccessibility} = utils;
+
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+
+ return parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+}
+
+function restoreBurgs(parentMap, projection, scale, pack, utils) {
+ const {d3, rn, isInMap, findCell, isWater, WARN, BurgsAndStates} = utils;
+
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ return parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, utils);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, utils) {
+ const {rn, BurgsAndStates} = utils;
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+}
+
+function restoreStates(parentMap, projection, pack, config, utils) {
+ const {rn, isInMap, findCell, BurgsAndStates} = utils;
+
+ const validStates = new Set(pack.cells.state);
+ let states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ states = states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords, config.graphWidth, config.graphHeight) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos, config.graphWidth, config.graphHeight) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos, config.graphWidth, config.graphHeight)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase, config.graphWidth, config.graphHeight) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+
+ return states;
+}
+
+function restoreRoutes(parentMap, projection, pack, config, utils) {
+ const {rn, isInMap, findCell, lineclip, Routes} = utils;
+
+ const routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config.graphWidth, config.graphHeight);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, config.graphWidth, config.graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(routes);
+ return routes;
+}
+
+function restoreReligions(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, getPolesOfInaccessibility} = utils;
+
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ return parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+}
+
+function restoreProvinces(parentMap, pack, utils) {
+ const {findCell, Provinces} = utils;
+
+ const validProvinces = new Set(pack.cells.province);
+ const provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+
+ return provinces;
+}
+
+function restoreMarkers(parentMap, projection, pack, utils) {
+ const {rn, isInMap, findCell, Markers} = utils;
+
+ const markers = parentMap.pack.markers;
+ markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+
+ return markers;
+}
+
+function restoreZones(parentMap, projection, scale, pack, utils) {
+ const {isInMap, findAll, unique} = utils;
+
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ return parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+}
+
+function restoreFeatureDetails(parentMap, inverse, pack, utils) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+
+ return pack.features;
+}
+
+function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+}
+
+function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/religions-generator_config.md b/procedural/src/engine/support/religions-generator_config.md
new file mode 100644
index 00000000..019a6080
--- /dev/null
+++ b/procedural/src/engine/support/religions-generator_config.md
@@ -0,0 +1,30 @@
+# Config Properties for religions-generator.js
+
+The refactored module requires the following configuration properties:
+
+## Required Config Properties
+
+### `religionsNumber`
+- **Type**: `number`
+- **Source**: Previously read from `religionsNumber.value` (DOM element)
+- **Description**: The desired number of organized religions to generate
+- **Usage**: Used in `generateOrganizedReligions()` to determine how many religions to create
+
+### `growthRate`
+- **Type**: `number`
+- **Source**: Previously read from `byId("growthRate").valueAsNumber` (DOM element)
+- **Description**: Growth rate multiplier that affects how far religions can expand
+- **Usage**: Used in `expandReligions()` to calculate `maxExpansionCost = (cells.i.length / 20) * config.growthRate`
+
+## Config Object Structure
+
+The config object should be structured as:
+
+```javascript
+const config = {
+ religionsNumber: 5, // Number of organized religions to generate
+ growthRate: 1.0 // Growth rate multiplier for religion expansion
+};
+```
+
+These properties replace the original DOM-based configuration reads and allow the engine to be run in any environment without browser dependencies.
\ No newline at end of file
diff --git a/procedural/src/engine/support/religions-generator_external.md b/procedural/src/engine/support/religions-generator_external.md
new file mode 100644
index 00000000..ab986862
--- /dev/null
+++ b/procedural/src/engine/support/religions-generator_external.md
@@ -0,0 +1,33 @@
+# External Dependencies for religions-generator.js
+
+The refactored module requires the following external dependencies to be imported or provided via the `utils` parameter:
+
+## Core Utilities
+- `TIME` - Boolean flag for timing console outputs
+- `WARN` - Boolean flag for warning console outputs
+- `ERROR` - Boolean flag for error console outputs
+- `rand(min, max)` - Random number generator
+- `ra(array)` - Random array element selector
+- `rw(weightedObject)` - Random weighted selection
+- `gauss(mean, deviation, min, max, step)` - Gaussian random number generator
+- `each(n)` - Function that returns a function checking if number is divisible by n
+
+## Data Structure Utilities
+- `d3.quadtree()` - D3 quadtree for spatial indexing
+- `FlatQueue` - Priority queue implementation
+- `Uint16Array` - Typed array constructor
+
+## Helper Functions
+- `getRandomColor()` - Generates random color
+- `getMixedColor(baseColor, saturation, lightness)` - Mixes colors
+- `abbreviate(name, existingCodes)` - Creates abbreviation codes
+- `trimVowels(string)` - Removes vowels from string
+- `getAdjective(string)` - Converts string to adjective form
+- `isWater(cellId)` - Checks if cell is water
+
+## External Modules
+- `Names.getCulture(culture, param1, param2, param3, param4)` - Name generation system
+- `Routes.getRoute(cellId1, cellId2)` - Route finding system
+- `biomesData.cost[biomeId]` - Biome traversal cost data
+
+All of these dependencies should be provided through the `utils` parameter when calling the exported functions.
\ No newline at end of file
diff --git a/procedural/src/engine/support/religions-generator_prompt.md b/procedural/src/engine/support/religions-generator_prompt.md
new file mode 100644
index 00000000..ab3d64d3
--- /dev/null
+++ b/procedural/src/engine/support/religions-generator_prompt.md
@@ -0,0 +1,1012 @@
+# religions-generator.js.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `religions-generator.js.js`.
+
+**File Content:**
+```javascript
+"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;
+ };
+
+ function updateCultures() {
+ pack.religions = pack.religions.map((religion, index) => {
+ if (index === 0) return religion;
+ return {...religion, culture: pack.cells.culture[religion.center]};
+ });
+ }
+
+ // 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, updateCultures, recalculate};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./religions-generator.js.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./religions-generator.js_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in religions-generator.js_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into religions-generator.js_render.md
diff --git a/procedural/src/engine/support/religions-generator_render.md b/procedural/src/engine/support/religions-generator_render.md
new file mode 100644
index 00000000..2281789a
--- /dev/null
+++ b/procedural/src/engine/support/religions-generator_render.md
@@ -0,0 +1,31 @@
+# Removed Rendering/UI Logic from religions-generator.js
+
+## Analysis Result
+
+**No rendering or UI logic was found in the original religions-generator.js module.**
+
+The original code was purely computational and focused on:
+
+1. **Data Generation**: Creating religion data structures
+2. **Algorithm Logic**: Implementing religion placement, expansion, and naming algorithms
+3. **Data Transformation**: Processing and organizing religion data
+4. **State Management**: Managing religion relationships and properties
+
+## What Was NOT Removed
+
+The code contained **zero** instances of:
+- DOM manipulation (no `document.getElementById`, `innerHTML`, etc.)
+- SVG rendering (no `d3.select`, path creation, etc.)
+- UI updates (no element styling, class additions, etc.)
+- Browser-specific APIs
+
+## Conclusion
+
+This module was already well-separated in terms of concerns - it handled pure data generation logic without any rendering responsibilities. The refactoring focused entirely on:
+
+- Converting from IIFE to ES modules
+- Replacing DOM-based configuration reads with config parameters
+- Implementing dependency injection for global state access
+- Making functions pure by returning new data instead of mutating globals
+
+No rendering logic needed to be extracted to a separate viewer component.
\ No newline at end of file
diff --git a/procedural/src/engine/support/resample_config.md b/procedural/src/engine/support/resample_config.md
new file mode 100644
index 00000000..a63e06ad
--- /dev/null
+++ b/procedural/src/engine/support/resample_config.md
@@ -0,0 +1,26 @@
+# Config Properties for resample.js
+
+The refactored `resample.js` module requires the following configuration properties to be passed in the `config` object:
+
+## Required Config Properties
+
+### Map Dimensions
+- `graphWidth` (Number) - The width of the map/graph canvas
+- `graphHeight` (Number) - The height of the map/graph canvas
+
+## Usage Context
+
+These configuration properties replace the original global variables that were accessed directly:
+
+### Original Global Access → Config Property
+- `graphWidth` → `config.graphWidth`
+- `graphHeight` → `config.graphHeight`
+
+## Notes
+
+The `graphWidth` and `graphHeight` properties are used extensively throughout the module for:
+- Boundary checking with `isInMap()` function calls
+- Creating bounding boxes for route clipping
+- Determining if projected coordinates fall within the map area
+
+These values were previously accessed as global variables but are now properly injected as configuration parameters following the dependency injection pattern.
\ No newline at end of file
diff --git a/procedural/src/engine/support/resample_external.md b/procedural/src/engine/support/resample_external.md
new file mode 100644
index 00000000..523b93f6
--- /dev/null
+++ b/procedural/src/engine/support/resample_external.md
@@ -0,0 +1,40 @@
+# External Module Dependencies for resample.js
+
+The refactored `resample.js` module requires the following external modules to be imported:
+
+## Engine Modules
+- `Features` - for `markupGrid()` and `markupPack()` methods
+- `Rivers` - for river processing methods like `addMeandering()`, `getBasin()`, `getApproximateLength()`
+- `BurgsAndStates` - for `getCloseToEdgePoint()` and `getPoles()` methods
+- `Routes` - for `buildLinks()` method
+- `Provinces` - for `getPoles()` method
+- `Markers` - for `deleteMarker()` method
+
+## Utility Functions Required
+The `utils` parameter should include:
+- `deepCopy` - for deep copying objects
+- `generateGrid` - for generating new grid
+- `rn` - for rounding numbers
+- `findCell` - for finding cell by coordinates
+- `findAll` - for finding all cells in radius
+- `isInMap` - for checking if coordinates are within map bounds
+- `unique` - for getting unique values from array
+- `lineclip` - for line clipping operations
+- `WARN` - warning flag for console logging
+- `d3` - D3.js library functions (quadtree, mean)
+- `isWater` - utility to check if cell is water
+- `getPolesOfInaccessibility` - for calculating poles of inaccessibility
+- `smoothHeightmap` - for smoothing heightmap data
+
+## Grid Processing Functions
+These functions need to be called externally after grid generation:
+- `addLakesInDeepDepressions()`
+- `openNearSeaLakes()`
+- `OceanLayers()`
+- `calculateMapCoordinates()`
+- `calculateTemperatures()`
+- `reGraph()`
+- `createDefaultRuler()`
+
+## Library Dependencies
+- D3.js - for quadtree operations and mathematical functions
\ No newline at end of file
diff --git a/procedural/src/engine/support/resample_prompt.md b/procedural/src/engine/support/resample_prompt.md
new file mode 100644
index 00000000..221de951
--- /dev/null
+++ b/procedural/src/engine/support/resample_prompt.md
@@ -0,0 +1,451 @@
+# resample.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `resample.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Resample = (function () {
+ /*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+ */
+ function process({projection, inverse, scale}) {
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(pack.rivers);
+
+ grid = generateGrid();
+ pack = {};
+ notes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale);
+
+ Features.markupGrid();
+ addLakesInDeepDepressions();
+ openNearSeaLakes();
+
+ OceanLayers();
+ calculateMapCoordinates();
+ calculateTemperatures();
+
+ reGraph();
+ Features.markupPack();
+ createDefaultRuler();
+
+ restoreCellData(parentMap, inverse, scale);
+ restoreRivers(riversData, projection, scale);
+ restoreCultures(parentMap, projection);
+ restoreBurgs(parentMap, projection, scale);
+ restoreStates(parentMap, projection);
+ restoreRoutes(parentMap, projection);
+ restoreReligions(parentMap, projection);
+ restoreProvinces(parentMap);
+ restoreFeatureDetails(parentMap, inverse);
+ restoreMarkers(parentMap, projection);
+ restoreZones(parentMap, projection, scale);
+
+ showStatistics();
+ }
+
+ function resamplePrimaryGridData(parentMap, inverse, scale) {
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap();
+ }
+
+ function smoothHeightmap() {
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+ }
+
+ function restoreCellData(parentMap, inverse, scale) {
+ pack.cells.biome = new Uint8Array(pack.cells.i.length);
+ pack.cells.fl = new Uint16Array(pack.cells.i.length);
+ pack.cells.s = new Int16Array(pack.cells.i.length);
+ pack.cells.pop = new Float32Array(pack.cells.i.length);
+ pack.cells.culture = new Uint16Array(pack.cells.i.length);
+ pack.cells.state = new Uint16Array(pack.cells.i.length);
+ pack.cells.burg = new Uint16Array(pack.cells.i.length);
+ pack.cells.religion = new Uint16Array(pack.cells.i.length);
+ pack.cells.province = new Uint16Array(pack.cells.i.length);
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+ }
+
+ function saveRiversData(parentRivers) {
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+ }
+
+ function restoreRivers(riversData, projection, scale) {
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ pack.rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ pack.rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+ }
+
+ function restoreCultures(parentMap, projection) {
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+ pack.cultures = parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+ }
+
+ function restoreBurgs(parentMap, projection, scale) {
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ pack.burgs = parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+ }
+
+ function restoreStates(parentMap, projection) {
+ const validStates = new Set(pack.cells.state);
+ pack.states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ pack.states = pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+ }
+
+ function restoreRoutes(parentMap, projection) {
+ pack.routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, graphWidth, graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(pack.routes);
+ }
+
+ function restoreReligions(parentMap, projection) {
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ pack.religions = parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+ }
+
+ function restoreProvinces(parentMap) {
+ const validProvinces = new Set(pack.cells.province);
+ pack.provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+ }
+
+ function restoreMarkers(parentMap, projection) {
+ pack.markers = parentMap.pack.markers;
+ pack.markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+ }
+
+ function restoreZones(parentMap, projection, scale) {
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ pack.zones = parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+ }
+
+ function restoreFeatureDetails(parentMap, inverse) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+ }
+
+ function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+ }
+
+ function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+ }
+
+ function isInMap(x, y) {
+ return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
+ }
+
+ return {process};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./resample.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./resample_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in resample_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into resample_render.md
diff --git a/procedural/src/engine/support/resample_render.md b/procedural/src/engine/support/resample_render.md
new file mode 100644
index 00000000..4292e50a
--- /dev/null
+++ b/procedural/src/engine/support/resample_render.md
@@ -0,0 +1,30 @@
+# Removed Rendering/UI Logic from resample.js
+
+The following rendering and UI-related code blocks were **removed** from the engine module and should be moved to the Viewer application:
+
+## Removed UI/Rendering Logic
+
+### Statistics Display
+```javascript
+// Line 117 in original code
+showStatistics();
+```
+
+**Description:** This function call was responsible for displaying statistics to the user interface after the resampling process completed. This is purely a UI/rendering concern and has been removed from the core engine.
+
+**Location in Original:** Called at the end of the `process()` function (line 117)
+
+**Reason for Removal:** This function updates the DOM/UI to show statistics about the generated map, which violates the separation of concerns principle for the headless engine.
+
+## Notes
+
+The original `resample.js` module was relatively clean in terms of separation of concerns. The only UI-related code was the single `showStatistics()` call, which was a clear DOM/UI interaction that needed to be removed from the core engine.
+
+All other code in the module was focused on data processing and transformation, which aligns well with the headless engine architecture.
+
+## Viewer Integration
+
+The Viewer application should:
+1. Call the engine's `process()` function to get the resampled map data
+2. Call `showStatistics()` with the returned data to update the UI
+3. Handle any other UI updates needed after resampling
\ No newline at end of file
diff --git a/procedural/src/engine/support/river-generator.txt b/procedural/src/engine/support/river-generator.txt
new file mode 100644
index 00000000..7c173884
--- /dev/null
+++ b/procedural/src/engine/support/river-generator.txt
@@ -0,0 +1,559 @@
+"use strict";
+
+export const generate = function (pack, grid, config, utils, modules, allowErosion = true) {
+ const {TIME, seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config;
+ const {rn, rw, each, round, d3, lineGen} = utils;
+ const {Lakes, Names} = modules;
+
+ TIME && console.time("generateRivers");
+ Math.random = aleaPRNG(seed);
+ const {cells, features} = pack;
+
+ const riversData = {}; // rivers data
+ const riverParents = {};
+
+ const addCellToRiver = function (cell, river) {
+ if (!riversData[river]) riversData[river] = [cell];
+ else riversData[river].push(cell);
+ };
+
+ const newCells = {
+ ...cells,
+ fl: new Uint16Array(cells.i.length), // water flux array
+ r: new Uint16Array(cells.i.length), // rivers array
+ conf: new Uint8Array(cells.i.length) // confluences array
+ };
+
+ let riverNext = 1; // first river id is 1
+
+ const h = alterHeights(pack, utils);
+ Lakes.detectCloseLakes(h);
+ const resolvedH = resolveDepressions(pack, config, utils, h);
+ const {updatedCells, updatedFeatures, updatedRivers} = drainWater(pack, grid, config, utils, modules, newCells, resolvedH, riversData, riverParents, riverNext);
+ const {finalCells, finalRivers} = defineRivers(pack, config, utils, updatedCells, riversData, riverParents);
+
+ calculateConfluenceFlux(finalCells, resolvedH);
+ Lakes.cleanupLakeData();
+
+ let finalH = resolvedH;
+ if (allowErosion) {
+ finalH = Uint8Array.from(resolvedH); // apply gradient
+ downcutRivers(pack, finalCells, finalH); // downcut river beds
+ }
+
+ TIME && console.timeEnd("generateRivers");
+
+ return {
+ pack: {
+ ...pack,
+ cells: {
+ ...pack.cells,
+ ...finalCells,
+ h: finalH
+ },
+ features: updatedFeatures,
+ rivers: finalRivers
+ }
+ };
+
+ function drainWater(pack, grid, config, utils, modules, cells, h, riversData, riverParents, riverNext) {
+ const {cellsCount} = config;
+ const {Lakes} = modules;
+ const MIN_FLUX_TO_FORM_RIVER = 30;
+ const cellsNumberModifier = (cellsCount / 10000) ** 0.25;
+
+ const prec = grid.cells.prec;
+ const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
+ const lakeOutCells = Lakes.defineClimateData(h);
+
+ land.forEach(function (i) {
+ cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
+
+ // create lake outlet if lake is not in deep depression and flux > evaporation
+ const lakes = lakeOutCells[i]
+ ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
+ : [];
+ for (const lake of lakes) {
+ const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
+ cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
+
+ // allow chain lakes to retain identity
+ if (cells.r[lakeCell] !== lake.river) {
+ const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
+
+ if (sameRiver) {
+ cells.r[lakeCell] = lake.river;
+ addCellToRiver(lakeCell, lake.river);
+ } else {
+ cells.r[lakeCell] = riverNext;
+ addCellToRiver(lakeCell, riverNext);
+ riverNext++;
+ }
+ }
+
+ lake.outlet = cells.r[lakeCell];
+ flowDown(i, cells.fl[lakeCell], lake.outlet);
+ }
+
+ // assign all tributary rivers to outlet basin
+ const outlet = lakes[0]?.outlet;
+ for (const lake of lakes) {
+ if (!Array.isArray(lake.inlets)) continue;
+ for (const inlet of lake.inlets) {
+ riverParents[inlet] = outlet;
+ }
+ }
+
+ // near-border cell: pour water out of the screen
+ if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
+
+ // downhill cell (make sure it's not in the source lake)
+ let min = null;
+ if (lakeOutCells[i]) {
+ const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
+ min = filtered.sort((a, b) => h[a] - h[b])[0];
+ } else if (cells.haven[i]) {
+ min = cells.haven[i];
+ } else {
+ min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
+ }
+
+ // cells is depressed
+ if (h[i] <= h[min]) return;
+
+ if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
+ // flux is too small to operate as a river
+ if (h[min] >= 20) cells.fl[min] += cells.fl[i];
+ return;
+ }
+
+ // proclaim a new river
+ if (!cells.r[i]) {
+ cells.r[i] = riverNext;
+ addCellToRiver(i, riverNext);
+ riverNext++;
+ }
+
+ flowDown(min, cells.fl[i], cells.r[i]);
+ });
+
+ function flowDown(toCell, fromFlux, river) {
+ const toFlux = cells.fl[toCell] - cells.conf[toCell];
+ const toRiver = cells.r[toCell];
+
+ if (toRiver) {
+ // downhill cell already has river assigned
+ if (fromFlux > toFlux) {
+ cells.conf[toCell] += cells.fl[toCell]; // mark confluence
+ if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
+ cells.r[toCell] = river; // re-assign river if downhill part has less flux
+ } else {
+ cells.conf[toCell] += fromFlux; // mark confluence
+ if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
+ }
+ } else cells.r[toCell] = river; // assign the river to the downhill cell
+
+ if (h[toCell] < 20) {
+ // pour water to the water body
+ const waterBody = features[cells.f[toCell]];
+ if (waterBody.type === "lake") {
+ if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
+ waterBody.river = river;
+ waterBody.enteringFlux = fromFlux;
+ }
+ waterBody.flux = waterBody.flux + fromFlux;
+ if (!waterBody.inlets) waterBody.inlets = [river];
+ else waterBody.inlets.push(river);
+ }
+ } else {
+ // propagate flux and add next river segment
+ cells.fl[toCell] += fromFlux;
+ }
+
+ addCellToRiver(toCell, river);
+ }
+
+ return {
+ updatedCells: cells,
+ updatedFeatures: features,
+ updatedRivers: []
+ };
+ }
+
+ function defineRivers(pack, config, utils, cells, riversData, riverParents) {
+ const {cellsCount} = config;
+ const {rn} = utils;
+
+ // re-initialize rivers and confluence arrays
+ const newCells = {
+ ...cells,
+ r: new Uint16Array(cells.i.length),
+ conf: new Uint16Array(cells.i.length)
+ };
+ const rivers = [];
+
+ const defaultWidthFactor = rn(1 / (cellsCount / 10000) ** 0.25, 2);
+ const mainStemWidthFactor = defaultWidthFactor * 1.2;
+
+ for (const key in riversData) {
+ const riverCells = riversData[key];
+ if (riverCells.length < 3) continue; // exclude tiny rivers
+
+ const riverId = +key;
+ for (const cell of riverCells) {
+ if (cell < 0 || cells.h[cell] < 20) continue;
+
+ // mark real confluences and assign river to cells
+ if (newCells.r[cell]) newCells.conf[cell] = 1;
+ else newCells.r[cell] = riverId;
+ }
+
+ const source = riverCells[0];
+ const mouth = riverCells[riverCells.length - 2];
+ const parent = riverParents[key] || 0;
+
+ const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
+ const meanderedPoints = addMeandering(pack, utils, riverCells);
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(utils, meanderedPoints);
+ const sourceWidth = getSourceWidth(utils, cells.fl[source]);
+ const width = getWidth(utils,
+ getOffset(utils, {
+ flux: discharge,
+ pointIndex: meanderedPoints.length,
+ widthFactor,
+ startingWidth: sourceWidth
+ })
+ );
+
+ rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth,
+ parent,
+ cells: riverCells
+ });
+ }
+
+ return {
+ finalCells: newCells,
+ finalRivers: rivers
+ };
+ }
+
+ function downcutRivers(pack, cells, h) {
+ const MAX_DOWNCUT = 5;
+
+ for (const i of pack.cells.i) {
+ if (cells.h[i] < 35) continue; // don't donwcut lowlands
+ if (!cells.fl[i]) continue;
+
+ const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
+ const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
+ if (!higherFlux) continue;
+
+ const downcut = Math.floor(cells.fl[i] / higherFlux);
+ if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
+ }
+ }
+
+ function calculateConfluenceFlux(cells, h) {
+ for (const i of cells.i) {
+ if (!cells.conf[i]) continue;
+
+ const sortedInflux = cells.c[i]
+ .filter(c => cells.r[c] && h[c] > h[i])
+ .map(c => cells.fl[c])
+ .sort((a, b) => b - a);
+ cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
+ }
+ }
+};
+
+// add distance to water value to land cells to make map less depressed
+export const alterHeights = (pack, utils) => {
+ const {d3} = utils;
+ const {h, c, t} = pack.cells;
+ return Array.from(h).map((h, i) => {
+ if (h < 20 || t[i] < 1) return h;
+ return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
+ });
+};
+
+// depression filling algorithm (for a correct water flux modeling)
+export const resolveDepressions = function (pack, config, utils, h) {
+ const {resolveDepressionsSteps, WARN} = config;
+ const {d3} = utils;
+ const {cells, features} = pack;
+ const maxIterations = resolveDepressionsSteps;
+ const checkLakeMaxIteration = maxIterations * 0.85;
+ const elevateLakeMaxIteration = maxIterations * 0.75;
+
+ const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
+
+ const lakes = features.filter(f => f.type === "lake");
+ const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
+ land.sort((a, b) => h[a] - h[b]); // lowest cells go first
+
+ const progress = [];
+ let depressions = Infinity;
+ let prevDepressions = null;
+ for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
+ if (progress.length > 5 && d3.sum(progress) > 0) {
+ // bad progress, abort and set heights back
+ h = alterHeights(pack, utils);
+ depressions = progress[0];
+ break;
+ }
+
+ depressions = 0;
+
+ if (iteration < checkLakeMaxIteration) {
+ for (const l of lakes) {
+ if (l.closed) continue;
+ const minHeight = d3.min(l.shoreline.map(s => h[s]));
+ if (minHeight >= 100 || l.height > minHeight) continue;
+
+ if (iteration > elevateLakeMaxIteration) {
+ l.shoreline.forEach(i => (h[i] = cells.h[i]));
+ l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
+ l.closed = true;
+ continue;
+ }
+
+ depressions++;
+ l.height = minHeight + 0.2;
+ }
+ }
+
+ for (const i of land) {
+ const minHeight = d3.min(cells.c[i].map(c => height(c)));
+ if (minHeight >= 100 || h[i] > minHeight) continue;
+
+ depressions++;
+ h[i] = minHeight + 0.1;
+ }
+
+ prevDepressions !== null && progress.push(depressions - prevDepressions);
+ prevDepressions = depressions;
+ }
+
+ depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
+ return h;
+};
+
+// add points at 1/3 and 2/3 of a line between adjacents river cells
+export const addMeandering = function (pack, utils, riverCells, riverPoints = null, meandering = 0.5) {
+ const {fl, h} = pack.cells;
+ const meandered = [];
+ const lastStep = riverCells.length - 1;
+ const points = getRiverPoints(pack, riverCells, riverPoints);
+ let step = h[riverCells[0]] < 20 ? 1 : 10;
+
+ for (let i = 0; i <= lastStep; i++, step++) {
+ const cell = riverCells[i];
+ const isLastCell = i === lastStep;
+
+ const [x1, y1] = points[i];
+
+ meandered.push([x1, y1, fl[cell]]);
+ if (isLastCell) break;
+
+ const nextCell = riverCells[i + 1];
+ const [x2, y2] = points[i + 1];
+
+ if (nextCell === -1) {
+ meandered.push([x2, y2, fl[cell]]);
+ break;
+ }
+
+ const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
+ if (dist2 <= 25 && riverCells.length >= 6) continue;
+
+ const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ const sinMeander = Math.sin(angle) * meander;
+ const cosMeander = Math.cos(angle) * meander;
+
+ if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
+ // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
+ const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
+ const p1y = (y1 * 2 + y2) / 3 + cosMeander;
+ const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
+ const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
+ meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
+ } else if (dist2 > 25 || riverCells.length < 6) {
+ // if dist is medium or river is small add 1 extra middlepoint
+ const p1x = (x1 + x2) / 2 + -sinMeander;
+ const p1y = (y1 + y2) / 2 + cosMeander;
+ meandered.push([p1x, p1y, 0]);
+ }
+ }
+
+ return meandered;
+};
+
+export const getRiverPoints = (pack, riverCells, riverPoints) => {
+ if (riverPoints) return riverPoints;
+
+ const {p} = pack.cells;
+ return riverCells.map((cell, i) => {
+ if (cell === -1) return getBorderPoint(pack, riverCells[i - 1]);
+ return p[cell];
+ });
+};
+
+export const getBorderPoint = (pack, config, i) => {
+ const {graphWidth, graphHeight} = config;
+ const [x, y] = pack.cells.p[i];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) return [x, 0];
+ else if (min === graphHeight - y) return [x, graphHeight];
+ else if (min === x) return [0, y];
+ return [graphWidth, y];
+};
+
+const FLUX_FACTOR = 500;
+const MAX_FLUX_WIDTH = 1;
+const LENGTH_FACTOR = 200;
+const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
+const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+
+export const getOffset = (utils, {flux, pointIndex, widthFactor, startingWidth}) => {
+ if (pointIndex === 0) return startingWidth;
+
+ const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || LENGTH_PROGRESSION.at(-1));
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+};
+
+export const getSourceWidth = (utils, flux) => {
+ const {rn} = utils;
+ return rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
+};
+
+// build polygon from a list of points and calculated offset (width)
+export const getRiverPath = (utils, points, widthFactor, startingWidth) => {
+ const {lineGen, d3, round} = utils;
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const riverPointsLeft = [];
+ const riverPointsRight = [];
+ let flux = 0;
+
+ for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
+ const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
+ const [x1, y1, pointFlux] = points[pointIndex];
+ const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
+ if (pointFlux > flux) flux = pointFlux;
+
+ const offset = getOffset(utils, {flux, pointIndex, widthFactor, startingWidth});
+ const angle = Math.atan2(y0 - y2, x0 - x2);
+ const sinOffset = Math.sin(angle) * offset;
+ const cosOffset = Math.cos(angle) * offset;
+
+ riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
+ riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
+ }
+
+ const right = lineGen(riverPointsRight.reverse());
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+
+ return round(right + left, 1);
+};
+
+export const specify = function (pack, modules, utils) {
+ const rivers = pack.rivers;
+ if (!rivers.length) return pack;
+
+ const updatedRivers = rivers.map(river => ({
+ ...river,
+ basin: getBasin(pack, river.i),
+ name: getName(pack, modules, river.mouth),
+ type: getType(pack, utils, river)
+ }));
+
+ return {
+ ...pack,
+ rivers: updatedRivers
+ };
+};
+
+export const getName = function (pack, modules, cell) {
+ const {Names} = modules;
+ return Names.getCulture(pack.cells.culture[cell]);
+};
+
+// weighted arrays of river type names
+const riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+};
+
+let smallLength = null;
+export const getType = function (pack, utils, {i, length, parent}) {
+ const {rw, each} = utils;
+ if (smallLength === null) {
+ const threshold = Math.ceil(pack.rivers.length * 0.15);
+ smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
+ }
+
+ const isSmall = length < smallLength;
+ const isFork = each(3)(i) && parent && parent !== i;
+ return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+};
+
+export const getApproximateLength = (utils, points) => {
+ const {rn} = utils;
+ const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+ return rn(length, 2);
+};
+
+// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
+// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
+export const getWidth = (utils, offset) => {
+ const {rn} = utils;
+ return rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+};
+
+// remove river and all its tributaries
+export const remove = function (pack, grid, id) {
+ const cells = pack.cells;
+ const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
+
+ // Update cells data
+ cells.r.forEach((r, i) => {
+ if (!r || !riversToRemove.includes(r)) return;
+ cells.r[i] = 0;
+ cells.fl[i] = grid.cells.prec[cells.g[i]];
+ cells.conf[i] = 0;
+ });
+
+ const updatedRivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+
+ return {
+ ...pack,
+ rivers: updatedRivers
+ };
+};
+
+export const getBasin = function (pack, r) {
+ const parent = pack.rivers.find(river => river.i === r)?.parent;
+ if (!parent || r === parent) return r;
+ return getBasin(pack, parent);
+};
+
+export const getNextId = function (rivers) {
+ return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
+};
\ No newline at end of file
diff --git a/procedural/src/engine/support/river-generator_config.md b/procedural/src/engine/support/river-generator_config.md
new file mode 100644
index 00000000..ee767064
--- /dev/null
+++ b/procedural/src/engine/support/river-generator_config.md
@@ -0,0 +1,66 @@
+# Configuration Properties for river-generator.js
+
+## DOM-Based Configuration Parameters Identified
+
+The refactored `river-generator.js` module identified the following DOM reads that were converted to config properties:
+
+### `resolveDepressionsSteps`
+- **Original DOM read:** `document.getElementById("resolveDepressionsStepsOutput").value` (line 330)
+- **Purpose:** Maximum number of iterations for the depression resolution algorithm
+- **Type:** Number (integer)
+- **Default suggestion:** 1000
+
+### Derived Configuration Parameters
+
+Additionally, the following parameters were extracted from global variables that should be configurable:
+
+### `cellsCount`
+- **Original source:** `pointsInput.dataset.cells` (lines 111, 240)
+- **Purpose:** Total number of cells in the map for calculations
+- **Type:** Number (integer)
+- **Usage:** Used in cellsNumberModifier calculations and width factor calculations
+
+### Graph Dimensions
+- **Original source:** Global `graphWidth` and `graphHeight` (lines 449, 453)
+- **Purpose:** Map boundaries for border point calculations
+- **Type:** Number
+- **Properties:** `graphWidth`, `graphHeight`
+
+### Flags and Constants
+- **Original source:** Global variables
+- **Properties:**
+ - `TIME` - Boolean flag for timing logs
+ - `WARN` - Boolean flag for warning messages
+ - `seed` - Random seed for reproducible generation
+ - `aleaPRNG` - Pseudo-random number generator function
+
+## Configuration Object Structure
+
+```javascript
+const config = {
+ // DOM-derived parameters
+ resolveDepressionsSteps: 1000, // Max iterations for depression resolution
+
+ // System parameters
+ cellsCount: 10000, // Total number of map cells
+ graphWidth: 1920, // Map width
+ graphHeight: 1080, // Map height
+
+ // Flags and utilities
+ TIME: true, // Enable timing logs
+ WARN: true, // Enable warning messages
+ seed: 'map_seed_123', // Random seed
+ aleaPRNG: seedrandom // PRNG function
+};
+```
+
+## Usage Notes
+
+1. **`resolveDepressionsSteps`** is critical for terrain depression resolution - higher values provide more accurate results but take longer to compute
+2. **`cellsCount`** affects river width calculations and flux modifiers
+3. **Graph dimensions** are essential for proper border calculations when rivers flow off the map
+4. **Timing flags** should be configurable for debugging and performance monitoring
+
+## Migration Impact
+
+This conversion removes the last DOM dependency from the river generation system, making it fully headless and environment-agnostic while maintaining all original functionality through proper configuration injection.
\ No newline at end of file
diff --git a/procedural/src/engine/support/river-generator_external.md b/procedural/src/engine/support/river-generator_external.md
new file mode 100644
index 00000000..fed98a31
--- /dev/null
+++ b/procedural/src/engine/support/river-generator_external.md
@@ -0,0 +1,95 @@
+# External Dependencies for river-generator.js
+
+The refactored `river-generator.js` module requires the following external dependencies to be imported:
+
+## Module Dependencies
+
+### `Lakes` module
+- **Functions used:**
+ - `Lakes.detectCloseLakes(h)` - Detect lakes close to each other
+ - `Lakes.defineClimateData(h)` - Define climate data for lakes
+ - `Lakes.cleanupLakeData()` - Clean up lake data after processing
+
+### `Names` module
+- **Functions used:**
+ - `Names.getCulture(cultureId)` - Get cultural names for rivers
+
+## Utility Functions Required
+
+The following utility functions need to be passed via the `utils` parameter:
+
+### Core Utilities
+- `rn(value, precision)` - Rounding function with precision
+- `rw(weightedObject)` - Random weighted selection from object
+- `each(n)` - Function that returns a function checking if value is divisible by n
+- `round(value, precision)` - General rounding function
+
+### D3.js Integration
+- `d3.mean(array)` - Calculate array mean
+- `d3.sum(array)` - Calculate array sum
+- `d3.min(array)` - Find minimum value in array
+- `d3.curveCatmullRom.alpha(value)` - D3 curve interpolation
+- `lineGen` - D3 line generator for creating SVG paths
+
+## Configuration Dependencies
+
+The following configuration values need to be passed via the `config` parameter:
+
+### Core Configuration
+- `TIME` - Boolean flag to enable/disable timing logs
+- `seed` - Random seed value for reproducible generation
+- `aleaPRNG` - Pseudo-random number generator function
+- `resolveDepressionsSteps` - Maximum iterations for depression resolution algorithm
+- `cellsCount` - Total number of cells in the map
+- `graphWidth` - Width of the map graph
+- `graphHeight` - Height of the map graph
+- `WARN` - Boolean flag to enable/disable warning messages
+
+## Module Integration
+
+The module should be imported and used as follows:
+
+```javascript
+import {
+ generate,
+ alterHeights,
+ resolveDepressions,
+ addMeandering,
+ getRiverPath,
+ specify,
+ getName,
+ getType,
+ getBasin,
+ getWidth,
+ getOffset,
+ getSourceWidth,
+ getApproximateLength,
+ getRiverPoints,
+ remove,
+ getNextId
+} from './river-generator.js';
+import { Lakes } from './lakes.js';
+import { Names } from './names.js';
+
+// Usage example
+const config = {
+ TIME: true,
+ seed: 'map_seed_123',
+ aleaPRNG: seedrandom,
+ resolveDepressionsSteps: 1000,
+ cellsCount: 10000,
+ graphWidth: 1920,
+ graphHeight: 1080,
+ WARN: true
+};
+
+const utils = {
+ rn, rw, each, round,
+ d3: { mean: d3.mean, sum: d3.sum, min: d3.min, curveCatmullRom: d3.curveCatmullRom },
+ lineGen: d3.line()
+};
+
+const modules = { Lakes, Names };
+
+const result = generate(pack, grid, config, utils, modules, true);
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/river-generator_prompt.md b/procedural/src/engine/support/river-generator_prompt.md
new file mode 100644
index 00000000..46c6ae21
--- /dev/null
+++ b/procedural/src/engine/support/river-generator_prompt.md
@@ -0,0 +1,602 @@
+# river-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `river-generator.js`.
+
+**File Content:**
+```javascript
+"use strict";
+
+window.Rivers = (function () {
+ const generate = function (allowErosion = true) {
+ TIME && console.time("generateRivers");
+ Math.random = aleaPRNG(seed);
+ const {cells, features} = pack;
+
+ const riversData = {}; // rivers data
+ const riverParents = {};
+
+ const addCellToRiver = function (cell, river) {
+ if (!riversData[river]) riversData[river] = [cell];
+ else riversData[river].push(cell);
+ };
+
+ cells.fl = new Uint16Array(cells.i.length); // water flux array
+ cells.r = new Uint16Array(cells.i.length); // rivers array
+ cells.conf = new Uint8Array(cells.i.length); // confluences array
+ let riverNext = 1; // first river id is 1
+
+ const h = alterHeights();
+ Lakes.detectCloseLakes(h);
+ resolveDepressions(h);
+ drainWater();
+ defineRivers();
+
+ calculateConfluenceFlux();
+ Lakes.cleanupLakeData();
+
+ if (allowErosion) {
+ cells.h = Uint8Array.from(h); // apply gradient
+ downcutRivers(); // downcut river beds
+ }
+
+ TIME && console.timeEnd("generateRivers");
+
+ function drainWater() {
+ const MIN_FLUX_TO_FORM_RIVER = 30;
+ const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
+
+ const prec = grid.cells.prec;
+ const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
+ const lakeOutCells = Lakes.defineClimateData(h);
+
+ land.forEach(function (i) {
+ cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
+
+ // create lake outlet if lake is not in deep depression and flux > evaporation
+ const lakes = lakeOutCells[i]
+ ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
+ : [];
+ for (const lake of lakes) {
+ const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
+ cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
+
+ // allow chain lakes to retain identity
+ if (cells.r[lakeCell] !== lake.river) {
+ const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
+
+ if (sameRiver) {
+ cells.r[lakeCell] = lake.river;
+ addCellToRiver(lakeCell, lake.river);
+ } else {
+ cells.r[lakeCell] = riverNext;
+ addCellToRiver(lakeCell, riverNext);
+ riverNext++;
+ }
+ }
+
+ lake.outlet = cells.r[lakeCell];
+ flowDown(i, cells.fl[lakeCell], lake.outlet);
+ }
+
+ // assign all tributary rivers to outlet basin
+ const outlet = lakes[0]?.outlet;
+ for (const lake of lakes) {
+ if (!Array.isArray(lake.inlets)) continue;
+ for (const inlet of lake.inlets) {
+ riverParents[inlet] = outlet;
+ }
+ }
+
+ // near-border cell: pour water out of the screen
+ if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
+
+ // downhill cell (make sure it's not in the source lake)
+ let min = null;
+ if (lakeOutCells[i]) {
+ const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
+ min = filtered.sort((a, b) => h[a] - h[b])[0];
+ } else if (cells.haven[i]) {
+ min = cells.haven[i];
+ } else {
+ min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
+ }
+
+ // cells is depressed
+ if (h[i] <= h[min]) return;
+
+ // debug
+ // .append("line")
+ // .attr("x1", pack.cells.p[i][0])
+ // .attr("y1", pack.cells.p[i][1])
+ // .attr("x2", pack.cells.p[min][0])
+ // .attr("y2", pack.cells.p[min][1])
+ // .attr("stroke", "#333")
+ // .attr("stroke-width", 0.2);
+
+ if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
+ // flux is too small to operate as a river
+ if (h[min] >= 20) cells.fl[min] += cells.fl[i];
+ return;
+ }
+
+ // proclaim a new river
+ if (!cells.r[i]) {
+ cells.r[i] = riverNext;
+ addCellToRiver(i, riverNext);
+ riverNext++;
+ }
+
+ flowDown(min, cells.fl[i], cells.r[i]);
+ });
+ }
+
+ function flowDown(toCell, fromFlux, river) {
+ const toFlux = cells.fl[toCell] - cells.conf[toCell];
+ const toRiver = cells.r[toCell];
+
+ if (toRiver) {
+ // downhill cell already has river assigned
+ if (fromFlux > toFlux) {
+ cells.conf[toCell] += cells.fl[toCell]; // mark confluence
+ if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
+ cells.r[toCell] = river; // re-assign river if downhill part has less flux
+ } else {
+ cells.conf[toCell] += fromFlux; // mark confluence
+ if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
+ }
+ } else cells.r[toCell] = river; // assign the river to the downhill cell
+
+ if (h[toCell] < 20) {
+ // pour water to the water body
+ const waterBody = features[cells.f[toCell]];
+ if (waterBody.type === "lake") {
+ if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
+ waterBody.river = river;
+ waterBody.enteringFlux = fromFlux;
+ }
+ waterBody.flux = waterBody.flux + fromFlux;
+ if (!waterBody.inlets) waterBody.inlets = [river];
+ else waterBody.inlets.push(river);
+ }
+ } else {
+ // propagate flux and add next river segment
+ cells.fl[toCell] += fromFlux;
+ }
+
+ addCellToRiver(toCell, river);
+ }
+
+ function defineRivers() {
+ // re-initialize rivers and confluence arrays
+ cells.r = new Uint16Array(cells.i.length);
+ cells.conf = new Uint16Array(cells.i.length);
+ pack.rivers = [];
+
+ const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
+ const mainStemWidthFactor = defaultWidthFactor * 1.2;
+
+ for (const key in riversData) {
+ const riverCells = riversData[key];
+ if (riverCells.length < 3) continue; // exclude tiny rivers
+
+ const riverId = +key;
+ for (const cell of riverCells) {
+ if (cell < 0 || cells.h[cell] < 20) continue;
+
+ // mark real confluences and assign river to cells
+ if (cells.r[cell]) cells.conf[cell] = 1;
+ else cells.r[cell] = riverId;
+ }
+
+ const source = riverCells[0];
+ const mouth = riverCells[riverCells.length - 2];
+ const parent = riverParents[key] || 0;
+
+ const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
+ const meanderedPoints = addMeandering(riverCells);
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(meanderedPoints);
+ const sourceWidth = getSourceWidth(cells.fl[source]);
+ const width = getWidth(
+ getOffset({
+ flux: discharge,
+ pointIndex: meanderedPoints.length,
+ widthFactor,
+ startingWidth: sourceWidth
+ })
+ );
+
+ pack.rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth,
+ parent,
+ cells: riverCells
+ });
+ }
+ }
+
+ function downcutRivers() {
+ const MAX_DOWNCUT = 5;
+
+ for (const i of pack.cells.i) {
+ if (cells.h[i] < 35) continue; // don't donwcut lowlands
+ if (!cells.fl[i]) continue;
+
+ const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
+ const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
+ if (!higherFlux) continue;
+
+ const downcut = Math.floor(cells.fl[i] / higherFlux);
+ if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
+ }
+ }
+
+ function calculateConfluenceFlux() {
+ for (const i of cells.i) {
+ if (!cells.conf[i]) continue;
+
+ const sortedInflux = cells.c[i]
+ .filter(c => cells.r[c] && h[c] > h[i])
+ .map(c => cells.fl[c])
+ .sort((a, b) => b - a);
+ cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
+ }
+ }
+ };
+
+ // add distance to water value to land cells to make map less depressed
+ const alterHeights = () => {
+ const {h, c, t} = pack.cells;
+ return Array.from(h).map((h, i) => {
+ if (h < 20 || t[i] < 1) return h;
+ return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
+ });
+ };
+
+ // depression filling algorithm (for a correct water flux modeling)
+ const resolveDepressions = function (h) {
+ const {cells, features} = pack;
+ const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
+ const checkLakeMaxIteration = maxIterations * 0.85;
+ const elevateLakeMaxIteration = maxIterations * 0.75;
+
+ const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
+
+ const lakes = features.filter(f => f.type === "lake");
+ const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
+ land.sort((a, b) => h[a] - h[b]); // lowest cells go first
+
+ const progress = [];
+ let depressions = Infinity;
+ let prevDepressions = null;
+ for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
+ if (progress.length > 5 && d3.sum(progress) > 0) {
+ // bad progress, abort and set heights back
+ h = alterHeights();
+ depressions = progress[0];
+ break;
+ }
+
+ depressions = 0;
+
+ if (iteration < checkLakeMaxIteration) {
+ for (const l of lakes) {
+ if (l.closed) continue;
+ const minHeight = d3.min(l.shoreline.map(s => h[s]));
+ if (minHeight >= 100 || l.height > minHeight) continue;
+
+ if (iteration > elevateLakeMaxIteration) {
+ l.shoreline.forEach(i => (h[i] = cells.h[i]));
+ l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
+ l.closed = true;
+ continue;
+ }
+
+ depressions++;
+ l.height = minHeight + 0.2;
+ }
+ }
+
+ for (const i of land) {
+ const minHeight = d3.min(cells.c[i].map(c => height(c)));
+ if (minHeight >= 100 || h[i] > minHeight) continue;
+
+ depressions++;
+ h[i] = minHeight + 0.1;
+ }
+
+ prevDepressions !== null && progress.push(depressions - prevDepressions);
+ prevDepressions = depressions;
+ }
+
+ depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
+ };
+
+ // add points at 1/3 and 2/3 of a line between adjacents river cells
+ const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
+ const {fl, h} = pack.cells;
+ const meandered = [];
+ const lastStep = riverCells.length - 1;
+ const points = getRiverPoints(riverCells, riverPoints);
+ let step = h[riverCells[0]] < 20 ? 1 : 10;
+
+ for (let i = 0; i <= lastStep; i++, step++) {
+ const cell = riverCells[i];
+ const isLastCell = i === lastStep;
+
+ const [x1, y1] = points[i];
+
+ meandered.push([x1, y1, fl[cell]]);
+ if (isLastCell) break;
+
+ const nextCell = riverCells[i + 1];
+ const [x2, y2] = points[i + 1];
+
+ if (nextCell === -1) {
+ meandered.push([x2, y2, fl[cell]]);
+ break;
+ }
+
+ const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
+ if (dist2 <= 25 && riverCells.length >= 6) continue;
+
+ const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ const sinMeander = Math.sin(angle) * meander;
+ const cosMeander = Math.cos(angle) * meander;
+
+ if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
+ // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
+ const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
+ const p1y = (y1 * 2 + y2) / 3 + cosMeander;
+ const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
+ const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
+ meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
+ } else if (dist2 > 25 || riverCells.length < 6) {
+ // if dist is medium or river is small add 1 extra middlepoint
+ const p1x = (x1 + x2) / 2 + -sinMeander;
+ const p1y = (y1 + y2) / 2 + cosMeander;
+ meandered.push([p1x, p1y, 0]);
+ }
+ }
+
+ return meandered;
+ };
+
+ const getRiverPoints = (riverCells, riverPoints) => {
+ if (riverPoints) return riverPoints;
+
+ const {p} = pack.cells;
+ return riverCells.map((cell, i) => {
+ if (cell === -1) return getBorderPoint(riverCells[i - 1]);
+ return p[cell];
+ });
+ };
+
+ const getBorderPoint = i => {
+ const [x, y] = pack.cells.p[i];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) return [x, 0];
+ else if (min === graphHeight - y) return [x, graphHeight];
+ else if (min === x) return [0, y];
+ return [graphWidth, y];
+ };
+
+ const FLUX_FACTOR = 500;
+ const MAX_FLUX_WIDTH = 1;
+ const LENGTH_FACTOR = 200;
+ const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
+ const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+
+ const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
+ if (pointIndex === 0) return startingWidth;
+
+ const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || LENGTH_PROGRESSION.at(-1));
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+ };
+
+ const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
+
+ // build polygon from a list of points and calculated offset (width)
+ const getRiverPath = (points, widthFactor, startingWidth) => {
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const riverPointsLeft = [];
+ const riverPointsRight = [];
+ let flux = 0;
+
+ for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
+ const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
+ const [x1, y1, pointFlux] = points[pointIndex];
+ const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
+ if (pointFlux > flux) flux = pointFlux;
+
+ const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
+ const angle = Math.atan2(y0 - y2, x0 - x2);
+ const sinOffset = Math.sin(angle) * offset;
+ const cosOffset = Math.cos(angle) * offset;
+
+ riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
+ riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
+ }
+
+ const right = lineGen(riverPointsRight.reverse());
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+
+ return round(right + left, 1);
+ };
+
+ const specify = function () {
+ const rivers = pack.rivers;
+ if (!rivers.length) return;
+
+ for (const river of rivers) {
+ river.basin = getBasin(river.i);
+ river.name = getName(river.mouth);
+ river.type = getType(river);
+ }
+ };
+
+ const getName = function (cell) {
+ return Names.getCulture(pack.cells.culture[cell]);
+ };
+
+ // weighted arrays of river type names
+ const riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+ };
+
+ let smallLength = null;
+ const getType = function ({i, length, parent}) {
+ if (smallLength === null) {
+ const threshold = Math.ceil(pack.rivers.length * 0.15);
+ smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
+ }
+
+ const isSmall = length < smallLength;
+ const isFork = each(3)(i) && parent && parent !== i;
+ return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+ };
+
+ const getApproximateLength = points => {
+ const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+ return rn(length, 2);
+ };
+
+ // Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
+ // Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
+ const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+
+ // remove river and all its tributaries
+ const remove = function (id) {
+ const cells = pack.cells;
+ const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
+ riversToRemove.forEach(r => rivers.select("#river" + r).remove());
+ cells.r.forEach((r, i) => {
+ if (!r || !riversToRemove.includes(r)) return;
+ cells.r[i] = 0;
+ cells.fl[i] = grid.cells.prec[cells.g[i]];
+ cells.conf[i] = 0;
+ });
+ pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+ };
+
+ const getBasin = function (r) {
+ const parent = pack.rivers.find(river => river.i === r)?.parent;
+ if (!parent || r === parent) return r;
+ return getBasin(parent);
+ };
+
+ const getNextId = function (rivers) {
+ return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
+ };
+
+ return {
+ generate,
+ alterHeights,
+ resolveDepressions,
+ addMeandering,
+ getRiverPath,
+ specify,
+ getName,
+ getType,
+ getBasin,
+ getWidth,
+ getOffset,
+ getSourceWidth,
+ getApproximateLength,
+ getRiverPoints,
+ remove,
+ getNextId
+ };
+})();
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./river-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./river-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in river-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into river-generator_render.md
diff --git a/procedural/src/engine/support/river-generator_render.md b/procedural/src/engine/support/river-generator_render.md
new file mode 100644
index 00000000..0695505b
--- /dev/null
+++ b/procedural/src/engine/support/river-generator_render.md
@@ -0,0 +1,100 @@
+# Removed Rendering/UI Logic from river-generator.js
+
+## Removed DOM Manipulation Code
+
+The following rendering and DOM manipulation code blocks were identified and **removed** from the engine module:
+
+### 1. River SVG Element Removal (Line 553)
+
+**Original Code:**
+```javascript
+riversToRemove.forEach(r => rivers.select("#river" + r).remove());
+```
+
+**Location:** In the `remove` function
+**Purpose:** Direct DOM manipulation to remove SVG river elements from the display
+**Removal Reason:** This is pure rendering logic that manipulates the DOM/SVG directly
+
+### 2. Debug SVG Line Drawing (Lines 173-179)
+
+**Original Code:**
+```javascript
+// debug
+// .append("line")
+// .attr("x1", pack.cells.p[i][0])
+// .attr("y1", pack.cells.p[i][1])
+// .attr("x2", pack.cells.p[min][0])
+// .attr("y2", pack.cells.p[min][1])
+// .attr("stroke", "#333")
+// .attr("stroke-width", 0.2);
+```
+
+**Location:** In the `drainWater` function
+**Purpose:** Debug visualization showing water flow directions as SVG lines
+**Removal Reason:** SVG rendering code for debugging visualization
+
+## Code Blocks That Were NOT Removed
+
+The following code blocks might appear to be rendering-related but were **retained** because they are computational:
+
+### `getRiverPath` Function
+- **Retained:** This function generates SVG path data as strings, but it's computational geometry
+- **Reasoning:** Path generation is part of the data model - the engine provides the path data, the viewer renders it
+
+### `lineGen` Usage
+- **Retained:** Used for mathematical path interpolation and curve generation
+- **Reasoning:** This is geometric computation, not direct DOM manipulation
+
+## Impact on Viewer Application
+
+The Viewer application will need to implement the following rendering features that were removed:
+
+### 1. River SVG Management
+```javascript
+// Viewer will need to implement:
+function removeRiverFromDOM(riverId) {
+ rivers.select(`#river${riverId}`).remove();
+}
+```
+
+### 2. Debug Visualization
+```javascript
+// Viewer can optionally implement debug lines:
+function renderDebugFlowLines(flowData) {
+ svg.selectAll('.debug-flow')
+ .data(flowData)
+ .enter()
+ .append('line')
+ .attr('class', 'debug-flow')
+ .attr('x1', d => d.from[0])
+ .attr('y1', d => d.from[1])
+ .attr('x2', d => d.to[0])
+ .attr('y2', d => d.to[1])
+ .attr('stroke', '#333')
+ .attr('stroke-width', 0.2);
+}
+```
+
+## Clean Separation Achieved
+
+The refactored module now maintains perfect separation:
+
+- **Engine Responsibilities:**
+ - River path computation and geometry
+ - Flow calculations and physics
+ - Data structure generation
+ - Mathematical algorithms
+
+- **Viewer Responsibilities (to be implemented):**
+ - SVG river rendering
+ - DOM element management
+ - Debug visualization
+ - User interface interactions
+
+## Summary
+
+**Total removed code blocks:** 2
+- 1 direct DOM manipulation (river element removal)
+- 1 debug SVG rendering (commented flow lines)
+
+The module is now completely headless and environment-agnostic, with all rendering logic successfully extracted for implementation in the Viewer application.
\ No newline at end of file
diff --git a/procedural/src/engine/support/routes-generator.txt b/procedural/src/engine/support/routes-generator.txt
new file mode 100644
index 00000000..71337176
--- /dev/null
+++ b/procedural/src/engine/support/routes-generator.txt
@@ -0,0 +1,712 @@
+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
+};
+
+export function generate(pack, grid, utils, lockedRoutes = []) {
+ const { dist2, findPath, findCell, rn } = utils;
+ 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();
+
+ const routes = createRoutesData(lockedRoutes);
+ const cellRoutes = buildLinks(routes);
+
+ return {
+ routes,
+ cellRoutes
+ };
+
+ function sortBurgsByFeature(burgs) {
+ const burgsByFeature = {};
+ const capitalsByFeature = {};
+ const portsByFeature = {};
+
+ const addBurg = (object, feature, burg) => {
+ if (!object[feature]) object[feature] = [];
+ object[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() {
+ 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 });
+ }
+ });
+ }
+
+ return mainRoads;
+ }
+
+ function 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 });
+ }
+ });
+ }
+
+ return trails;
+ }
+
+ function 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 });
+ }
+ });
+ }
+
+ 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 = utils.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 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 } = utils.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;
+ }
+}
+
+export 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;
+}
+
+// connect cell with routes system by land
+export function connect(cellId, pack, utils) {
+ const { findPath } = utils;
+ const getCost = createCostEvaluator({ isWater: false, connections: new Map() });
+ const pathCells = findPath(cellId, isConnected, getCost);
+ if (!pathCells) return null;
+
+ const pointsArray = preparePointsArray();
+ const points = getPoints("trails", pathCells, pointsArray);
+ const feature = pack.cells.f[cellId];
+ const routeId = getNextId(pack.routes);
+ const newRoute = { i: routeId, group: "trails", feature, points };
+
+ const connections = [];
+ for (let i = 0; i < pathCells.length; i++) {
+ const from = pathCells[i];
+ const to = pathCells[i + 1];
+ if (to) connections.push({ from, to, routeId });
+ }
+
+ return { route: newRoute, connections };
+
+ function createCostEvaluator({ isWater, connections }) {
+ const { dist2 } = utils;
+ return isWater ? getWaterPathCost : getLandPathCost;
+
+ function getLandPathCost(current, next) {
+ if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
+
+ const habitability = utils.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 (utils.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 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 { rn, findCell } = utils;
+ 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 isConnected(cellId) {
+ const routes = pack.cells.routes;
+ return routes[cellId] && Object.keys(routes[cellId]).length > 0;
+ }
+}
+
+// utility functions
+export function isConnected(cellId, pack) {
+ const routes = pack.cells.routes;
+ return routes[cellId] && Object.keys(routes[cellId]).length > 0;
+}
+
+export function areConnected(from, to, pack) {
+ const routeId = pack.cells.routes[from]?.[to];
+ return routeId !== undefined;
+}
+
+export function getRoute(from, to, pack) {
+ 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;
+}
+
+export function hasRoad(cellId, pack) {
+ 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";
+ });
+}
+
+export function isCrossroad(cellId, pack) {
+ 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;
+}
+
+// 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 }
+};
+
+export function generateName({ group, points }, pack, utils) {
+ const { ra, rw, getAdjective } = utils;
+
+ 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;
+ }
+}
+
+export function getNextId(routes) {
+ return routes.length ? Math.max(...routes.map(r => r.i)) + 1 : 0;
+}
+
+export function remove(route, pack) {
+ const routes = pack.cells.routes;
+ const removedConnections = [];
+
+ 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) {
+ removedConnections.push({ from, to });
+ }
+ }
+ }
+
+ const updatedRoutes = pack.routes.filter(r => r.i !== route.i);
+ const updatedCellRoutes = { ...routes };
+
+ removedConnections.forEach(({ from, to }) => {
+ if (updatedCellRoutes[from]) delete updatedCellRoutes[from][to];
+ if (updatedCellRoutes[to]) delete updatedCellRoutes[to][from];
+ });
+
+ return {
+ routes: updatedRoutes,
+ cellRoutes: updatedCellRoutes,
+ removedConnections
+ };
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/routes-generator_config.md b/procedural/src/engine/support/routes-generator_config.md
new file mode 100644
index 00000000..10585e87
--- /dev/null
+++ b/procedural/src/engine/support/routes-generator_config.md
@@ -0,0 +1,20 @@
+# Configuration Properties for routes-generator.js
+
+After analyzing the original `routes-generator.js` code, **no DOM-based configuration parameters were found**.
+
+The module does not contain any `byId()` calls or direct DOM reads that would require configuration properties.
+
+All configuration is done through:
+- Constants defined at the module level (ROUTES_SHARP_ANGLE, MIN_PASSABLE_SEA_TEMP, etc.)
+- Data passed in through function parameters (pack, grid, lockedRoutes)
+- Utility functions passed through the utils object
+
+Therefore, no `config` object properties need to be defined for this module.
+
+## Constants Used (Internal to Module)
+- `ROUTES_SHARP_ANGLE = 135`
+- `ROUTES_VERY_SHARP_ANGLE = 115`
+- `MIN_PASSABLE_SEA_TEMP = -4`
+- `ROUTE_TYPE_MODIFIERS` object with water type modifiers
+
+These are hardcoded constants and do not require external configuration.
\ No newline at end of file
diff --git a/procedural/src/engine/support/routes-generator_external.md b/procedural/src/engine/support/routes-generator_external.md
new file mode 100644
index 00000000..0389dd4a
--- /dev/null
+++ b/procedural/src/engine/support/routes-generator_external.md
@@ -0,0 +1,36 @@
+# External Dependencies for routes-generator.js
+
+The refactored `routes-generator.js` module requires the following external dependencies to be imported:
+
+## Utility Functions
+- `dist2` - Distance calculation function
+- `findPath` - Pathfinding algorithm function
+- `findCell` - Cell lookup function by coordinates
+- `rn` - Number rounding utility
+- `ra` - Random array element selection
+- `rw` - Weighted random selection
+- `getAdjective` - Name transformation utility
+
+## External Libraries
+- `Delaunator` - Delaunay triangulation library for Urquhart edge calculation
+
+## Data Dependencies
+- `biomesData` - Object containing biome information with habitability data
+
+## Grid Data
+- `grid` - Grid object containing temperature data for cells
+
+These dependencies should be passed through a `utils` object parameter that contains:
+```javascript
+{
+ dist2,
+ findPath,
+ findCell,
+ rn,
+ ra,
+ rw,
+ getAdjective,
+ Delaunator,
+ biomesData
+}
+```
\ No newline at end of file
diff --git a/procedural/src/engine/support/routes-generator_prompt.md b/procedural/src/engine/support/routes-generator_prompt.md
new file mode 100644
index 00000000..32045ab0
--- /dev/null
+++ b/procedural/src/engine/support/routes-generator_prompt.md
@@ -0,0 +1,737 @@
+# routes-generator.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `routes-generator.js`.
+
+**File Content:**
+```javascript
+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 = (object, feature, burg) => {
+ if (!object[feature]) object[feature] = [];
+ object[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 pathCells = findPath(cellId, isConnected, 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 cellId = pathCells[i];
+ const nextCellId = pathCells[i + 1];
+ if (nextCellId) addConnection(cellId, 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;
+ }
+
+ // 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,
+ generateName,
+ getPath,
+ getLength,
+ getNextId,
+ remove
+ };
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./routes-generator.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./routes-generator_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in routes-generator_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into routes-generator_render.md
diff --git a/procedural/src/engine/support/routes-generator_render.md b/procedural/src/engine/support/routes-generator_render.md
new file mode 100644
index 00000000..a551e4e7
--- /dev/null
+++ b/procedural/src/engine/support/routes-generator_render.md
@@ -0,0 +1,57 @@
+# Removed Rendering/UI Logic from routes-generator.js
+
+The following DOM manipulation and SVG rendering code blocks were **removed** from the engine module and should be moved to the Viewer application:
+
+## 1. Route Path Generation (SVG)
+**Location:** `getPath()` function (lines 675-680)
+```javascript
+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;
+}
+```
+
+## 2. Route Length Measurement (DOM Access)
+**Location:** `getLength()` function (lines 682-685)
+```javascript
+function getLength(routeId) {
+ const path = routes.select("#route" + routeId).node();
+ return path.getTotalLength();
+}
+```
+
+## 3. Route Removal from SVG (DOM Manipulation)
+**Location:** `remove()` function (line 707)
+```javascript
+// From the original remove() function:
+viewbox.select("#route" + route.i).remove();
+```
+
+## 4. Console Timing (UI Feedback)
+**Location:** Throughout generation functions
+```javascript
+// Lines 121, 139, 144, 162, 167, 185
+TIME && console.time("generateMainRoads");
+TIME && console.timeEnd("generateMainRoads");
+TIME && console.time("generateTrails");
+TIME && console.timeEnd("generateTrails");
+TIME && console.time("generateSeaRoutes");
+TIME && console.timeEnd("generateSeaRoutes");
+```
+
+## Summary
+- **SVG path generation using D3** - Should be handled by the Viewer
+- **DOM element selection and manipulation** - Should be handled by the Viewer
+- **Console timing output** - Should be handled by the Viewer for debugging
+- **Direct access to SVG elements** - Should be handled by the Viewer
+
+The engine now returns pure data that the Viewer can use to create SVG paths and handle all rendering operations.
\ No newline at end of file
diff --git a/procedural/src/engine/support/submap.txt b/procedural/src/engine/support/submap.txt
new file mode 100644
index 00000000..6e79556b
--- /dev/null
+++ b/procedural/src/engine/support/submap.txt
@@ -0,0 +1,375 @@
+"use strict";
+
+/*
+ generate new map based on an existing one (resampling parentMap)
+ parentMap: {grid, pack, notes} from original map
+ projection: f(Number, Number) -> [Number, Number]
+ inverse: f(Number, Number) -> [Number, Number]
+ scale: Number
+*/
+export function process({projection, inverse, scale}, grid, pack, notes, config, utils) {
+ const {
+ deepCopy, generateGrid, Features, addLakesInDeepDepressions, openNearSeaLakes,
+ OceanLayers, calculateMapCoordinates, calculateTemperatures, reGraph,
+ createDefaultRuler, Rivers, BurgsAndStates, Routes, Provinces, Markers,
+ isWater, findCell, findAll, rn, unique, d3, lineclip, getPolesOfInaccessibility,
+ WARN
+ } = utils;
+
+ const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
+ const riversData = saveRiversData(parentMap.pack.rivers, Rivers);
+
+ const newGrid = generateGrid();
+ const newPack = {};
+ const newNotes = parentMap.notes;
+
+ resamplePrimaryGridData(parentMap, inverse, scale, newGrid, d3, isWater);
+
+ Features.markupGrid(newGrid);
+ addLakesInDeepDepressions(newGrid);
+ openNearSeaLakes(newGrid);
+
+ OceanLayers(newGrid);
+ calculateMapCoordinates(newGrid);
+ calculateTemperatures(newGrid);
+
+ reGraph(newGrid, newPack);
+ Features.markupPack(newPack);
+ createDefaultRuler(newPack);
+
+ restoreCellData(parentMap, inverse, scale, newPack, d3, isWater, groupCellsByType);
+ restoreRivers(riversData, projection, scale, newPack, isInMap, findCell, rn, Rivers, config);
+ restoreCultures(parentMap, projection, newPack, getPolesOfInaccessibility, findCell, rn, isInMap, config);
+ restoreBurgs(parentMap, projection, scale, newPack, d3, groupCellsByType, findCell, isWater, isInMap, rn, BurgsAndStates, WARN, config);
+ restoreStates(parentMap, projection, newPack, BurgsAndStates, findCell, isInMap, rn, config);
+ restoreRoutes(parentMap, projection, newPack, isInMap, rn, findCell, lineclip, Routes, config);
+ restoreReligions(parentMap, projection, newPack, getPolesOfInaccessibility, findCell, rn, isInMap, config);
+ restoreProvinces(parentMap, newPack, Provinces, findCell);
+ restoreFeatureDetails(parentMap, inverse, newPack);
+ restoreMarkers(parentMap, projection, newPack, isInMap, findCell, rn, Markers);
+ restoreZones(parentMap, projection, scale, newPack, isInMap, findCell, findAll, unique);
+
+ return {
+ grid: newGrid,
+ pack: newPack,
+ notes: newNotes
+ };
+}
+
+function resamplePrimaryGridData(parentMap, inverse, scale, grid, d3, isWater) {
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ grid.points.forEach(([x, y], newGridCell) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) smoothHeightmap(grid, d3, isWater);
+}
+
+function smoothHeightmap(grid, d3, isWater) {
+ grid.cells.h.forEach((height, newGridCell) => {
+ const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
+ const meanHeight = d3.mean(heights);
+ grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
+ });
+}
+
+function restoreCellData(parentMap, inverse, scale, pack, d3, isWater, groupCellsByType) {
+ pack.cells.biome = new Uint8Array(pack.cells.i.length);
+ pack.cells.fl = new Uint16Array(pack.cells.i.length);
+ pack.cells.s = new Int16Array(pack.cells.i.length);
+ pack.cells.pop = new Float32Array(pack.cells.i.length);
+ pack.cells.culture = new Uint16Array(pack.cells.i.length);
+ pack.cells.state = new Uint16Array(pack.cells.i.length);
+ pack.cells.burg = new Uint16Array(pack.cells.i.length);
+ pack.cells.religion = new Uint16Array(pack.cells.i.length);
+ pack.cells.province = new Uint16Array(pack.cells.i.length);
+
+ const parentPackCellGroups = groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(pack, newPackCell)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
+ pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
+ pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
+ pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
+ pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
+ }
+}
+
+function saveRiversData(parentRivers, Rivers) {
+ return parentRivers.map(river => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return {...river, meanderedPoints};
+ });
+}
+
+function restoreRivers(riversData, projection, scale, pack, isInMap, findCell, rn, Rivers, config) {
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ pack.rivers = riversData
+ .map(river => {
+ let wasInMap = true;
+ const points = [];
+
+ river.meanderedPoints.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points.map(point => findCell(...point));
+ cells.forEach(cellId => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
+ })
+ .filter(Boolean);
+
+ pack.rivers.forEach(river => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+}
+
+function restoreCultures(parentMap, projection, pack, getPolesOfInaccessibility, findCell, rn, isInMap, config) {
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
+ pack.cultures = parentMap.pack.cultures.map(culture => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y, config) ? [x, y] : culturePoles[culture.i];
+ const center = findCell(...centerCoords);
+ return {...culture, center};
+ });
+}
+
+function restoreBurgs(parentMap, projection, scale, pack, d3, groupCellsByType, findCell, isWater, isInMap, rn, BurgsAndStates, WARN, config) {
+ const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
+ const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ pack.burgs = parentMap.pack.burgs.map(burg => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!isInMap(xp, yp, config)) return {...burg, removed: true, lock: false};
+
+ const closestCell = findCell(xp, yp);
+ const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
+ return {...burg, removed: true, lock: false};
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, BurgsAndStates, rn);
+ return {...burg, cell, x, y};
+ });
+
+ function getBurgCoordinates(burg, closestCell, cell, xp, yp, pack, BurgsAndStates, rn) {
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+}
+
+function restoreStates(parentMap, projection, pack, BurgsAndStates, findCell, isInMap, rn, config) {
+ const validStates = new Set(pack.cells.state);
+ pack.states = parentMap.pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return {...state, removed: true, lock: false};
+ });
+
+ BurgsAndStates.getPoles();
+ const regimentCellsMap = {};
+ const VERTICAL_GAP = 8;
+
+ pack.states = pack.states.map(state => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
+
+ const military = state.military.map(regiment => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = isInMap(...cellCoords, config) ? findCell(...cellCoords) : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ isInMap(xPos, yPos, config) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
+
+ const pos = isInMap(xPos, yPos, config)
+ ? {x: rn(xPos, 2), y: rn(yPos, 2)}
+ : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
+
+ const base = isInMap(xBase, yBase, config) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
+
+ return {...regiment, cell, name, ...base, ...pos};
+ });
+
+ const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
+ return {...state, neighbors, military};
+ });
+}
+
+function restoreRoutes(parentMap, projection, pack, isInMap, rn, findCell, lineclip, Routes, config) {
+ pack.routes = parentMap.pack.routes
+ .map(route => {
+ let wasInMap = true;
+ const points = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = isInMap(x, y, config);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox = [0, 0, config.graphWidth, config.graphHeight];
+ const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return {...route, feature, points: clipped};
+ })
+ .filter(Boolean);
+
+ pack.cells.routes = Routes.buildLinks(pack.routes);
+}
+
+function restoreReligions(parentMap, projection, pack, getPolesOfInaccessibility, findCell, rn, isInMap, config) {
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
+
+ pack.religions = parentMap.pack.religions.map(religion => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const centerCoords = isInMap(x, y, config) ? [x, y] : religionPoles[religion.i];
+ const center = findCell(...centerCoords);
+ return {...religion, center};
+ });
+}
+
+function restoreProvinces(parentMap, pack, Provinces, findCell) {
+ const validProvinces = new Set(pack.cells.province);
+ pack.provinces = parentMap.pack.provinces.map(province => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
+ });
+}
+
+function restoreMarkers(parentMap, projection, pack, isInMap, findCell, rn, Markers) {
+ pack.markers = parentMap.pack.markers;
+ pack.markers.forEach(marker => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findCell(x, y);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+}
+
+function restoreZones(parentMap, projection, scale, pack, isInMap, findCell, findAll, unique) {
+ const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ pack.zones = parentMap.pack.zones.map(zone => {
+ const cells = zone.cells
+ .map(cellId => {
+ const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!isInMap(x, y)) return null;
+ return findAll(x, y, getSearchRadius(cellId));
+ })
+ .filter(Boolean)
+ .flat();
+
+ return {...zone, cells: unique(cells)};
+ });
+}
+
+function restoreFeatureDetails(parentMap, inverse, pack) {
+ pack.features.forEach(feature => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
+ if (parentCell === undefined) return;
+ const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+}
+
+function groupCellsByType(graph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(graph, cellId) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ {land: [], water: []}
+ );
+}
+
+function isWater(graph, cellId) {
+ return graph.cells.h[cellId] < 20;
+}
+
+function isInMap(x, y, config) {
+ return x >= 0 && x <= config.graphWidth && y >= 0 && y <= config.graphHeight;
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/submap_config.md b/procedural/src/engine/support/submap_config.md
new file mode 100644
index 00000000..3beffe03
--- /dev/null
+++ b/procedural/src/engine/support/submap_config.md
@@ -0,0 +1,32 @@
+# Config Properties for submap.js
+
+The refactored `submap.js` module requires the following configuration properties:
+
+## Required Config Properties
+
+### `graphWidth` (Number)
+- **Purpose**: Width of the map canvas/viewport
+- **Usage**: Used in `isInMap()` function to determine if coordinates are within map boundaries
+- **Original source**: Global variable `graphWidth`
+
+### `graphHeight` (Number)
+- **Purpose**: Height of the map canvas/viewport
+- **Usage**: Used in `isInMap()` function to determine if coordinates are within map boundaries
+- **Original source**: Global variable `graphHeight`
+
+## Configuration Object Structure
+
+The config object should be structured as:
+
+```javascript
+const config = {
+ graphWidth: 1920, // Map canvas width
+ graphHeight: 1080 // Map canvas height
+};
+```
+
+## Notes
+
+- These properties were originally accessed as global variables in the legacy code
+- The `isInMap(x, y, config)` function now uses `config.graphWidth` and `config.graphHeight` instead of global variables
+- These values are critical for proper coordinate validation during map resampling operations
\ No newline at end of file
diff --git a/procedural/src/engine/support/submap_external.md b/procedural/src/engine/support/submap_external.md
new file mode 100644
index 00000000..dafb5190
--- /dev/null
+++ b/procedural/src/engine/support/submap_external.md
@@ -0,0 +1,31 @@
+# External Module Dependencies for submap.js
+
+The refactored `submap.js` module requires the following external modules to be imported:
+
+## Core Engine Modules
+- `Features` - For grid and pack markup operations
+- `Rivers` - For river restoration and management
+- `BurgsAndStates` - For burg and state restoration
+- `Routes` - For route restoration
+- `Provinces` - For province restoration
+- `Markers` - For marker management
+
+## Utility Functions (passed via utils object)
+- `deepCopy` - For creating deep copies of objects
+- `generateGrid` - For generating new grid structure
+- `addLakesInDeepDepressions` - For lake generation
+- `openNearSeaLakes` - For lake processing
+- `OceanLayers` - For ocean layer processing
+- `calculateMapCoordinates` - For coordinate calculations
+- `calculateTemperatures` - For temperature calculations
+- `reGraph` - For graph regeneration
+- `createDefaultRuler` - For ruler creation
+- `getPolesOfInaccessibility` - For calculating geometric poles
+- `isWater` - Utility function to check if cell is water
+- `findCell` - Function to find cell by coordinates
+- `findAll` - Function to find all cells in radius
+- `rn` - Rounding utility function
+- `unique` - Array deduplication utility
+- `d3` - D3.js library for quadtree operations
+- `lineclip` - Line clipping utility
+- `WARN` - Warning flag for console output
\ No newline at end of file
diff --git a/procedural/src/engine/support/submap_prompt.md b/procedural/src/engine/support/submap_prompt.md
new file mode 100644
index 00000000..91c9e82d
--- /dev/null
+++ b/procedural/src/engine/support/submap_prompt.md
@@ -0,0 +1,83 @@
+# submap.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `submap.js`.
+
+**File Content:**
+```javascript
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./submap.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./submap_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in submap_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into submap_render.md
diff --git a/procedural/src/engine/support/submap_render.md b/procedural/src/engine/support/submap_render.md
new file mode 100644
index 00000000..b1eafdf5
--- /dev/null
+++ b/procedural/src/engine/support/submap_render.md
@@ -0,0 +1,24 @@
+# Removed Rendering/UI Logic from submap.js
+
+The following UI/DOM manipulation code was **removed** from the engine module and should be moved to the Viewer application:
+
+## Removed Code Blocks
+
+### 1. Statistics Display (Line 116)
+```javascript
+showStatistics();
+```
+
+**Description**: This function call displays statistics to the user interface, likely updating DOM elements to show information about the submap map.
+
+**Reason for removal**: This is pure UI logic that renders information to the user interface and has no place in a headless engine.
+
+**Viewer implementation needed**: The Viewer application should call `showStatistics()` after receiving the processed map data from the engine.
+
+## Summary
+
+Only **one** piece of rendering/UI logic was found and removed from this module:
+
+- **UI Statistics Display**: The `showStatistics()` function call that displays map statistics to the user interface
+
+The refactored engine module now returns the processed map data (`{grid, pack, notes}`) and leaves all UI responsibilities to the calling Viewer application.
\ No newline at end of file
diff --git a/procedural/src/engine/support/voronoi.txt b/procedural/src/engine/support/voronoi.txt
new file mode 100644
index 00000000..d73bbb8e
--- /dev/null
+++ b/procedural/src/engine/support/voronoi.txt
@@ -0,0 +1,135 @@
+export class Voronoi {
+ /**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+ constructor(delaunay, points, pointsN) {
+ this.delaunay = delaunay;
+ this.points = points;
+ this.pointsN = pointsN;
+ this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
+ this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+
+ // Half-edges are the indices into the delaunator outputs:
+ // delaunay.triangles[e] gives the point ID where the half-edge starts
+ // delaunay.halfedges[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
+ for (let e = 0; e < this.delaunay.triangles.length; e++) {
+
+ const p = this.delaunay.triangles[this.nextHalfedge(e)];
+ if (p < this.pointsN && !this.cells.c[p]) {
+ const edges = this.edgesAroundPoint(e);
+ this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
+ this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
+ this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
+ }
+
+ const t = this.triangleOfEdge(e);
+ if (!this.vertices.p[t]) {
+ this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
+ this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
+ this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
+ }
+ }
+ }
+
+ /**
+ * Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The IDs of the points comprising the given triangle.
+ */
+ pointsOfTriangle(t) {
+ return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ }
+
+ /**
+ * Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {number[]} The indices of the triangles that share half-edges with this triangle.
+ */
+ trianglesAdjacentToTriangle(t) {
+ let triangles = [];
+ for (let edge of this.edgesOfTriangle(t)) {
+ let opposite = this.delaunay.halfedges[edge];
+ triangles.push(this.triangleOfEdge(opposite));
+ }
+ return triangles;
+ }
+
+ /**
+ * Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
+ * @param {number} start The index of an incoming half-edge that leads to the desired point
+ * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ */
+ edgesAroundPoint(start) {
+ const result = [];
+ let incoming = start;
+ do {
+ result.push(incoming);
+ const outgoing = this.nextHalfedge(incoming);
+ incoming = this.delaunay.halfedges[outgoing];
+ } while (incoming !== -1 && incoming !== start && result.length < 20);
+ return result;
+ }
+
+ /**
+ * Returns the center of the triangle located at the given index.
+ * @param {number} t The index of the triangle
+ * @returns {[number, number]}
+ */
+ triangleCenter(t) {
+ let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ return this.circumcenter(vertices[0], vertices[1], vertices[2]);
+ }
+
+ /**
+ * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The edges of the triangle.
+ */
+ edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+
+ /**
+ * Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} e The index of the edge
+ * @returns {number} The index of the triangle
+ */
+ triangleOfEdge(e) { return Math.floor(e / 3); }
+
+ /**
+ * Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the next half edge
+ */
+ nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+
+ /**
+ * Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the previous half edge
+ */
+ prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+
+ /**
+ * Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
+ * @param {[number, number]} a The coordinates of the first point of the triangle
+ * @param {[number, number]} b The coordinates of the second point of the triangle
+ * @param {[number, number]} c The coordinates of the third point of the triangle
+ * @return {[number, number]} The coordinates of the circumcenter of the triangle.
+ */
+ circumcenter(a, b, c) {
+ const [ax, ay] = a;
+ const [bx, by] = b;
+ const [cx, cy] = c;
+ const ad = ax * ax + ay * ay;
+ const bd = bx * bx + by * by;
+ const cd = cx * cx + cy * cy;
+ const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
+ return [
+ Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
+ Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
+ ];
+ }
+}
\ No newline at end of file
diff --git a/procedural/src/engine/support/voronoi_config.md b/procedural/src/engine/support/voronoi_config.md
new file mode 100644
index 00000000..6053d1bc
--- /dev/null
+++ b/procedural/src/engine/support/voronoi_config.md
@@ -0,0 +1,15 @@
+# Config Properties for voronoi.js
+
+The refactored `voronoi.js` module requires **no config properties**.
+
+## Analysis:
+- The Voronoi class is a pure geometric computation module
+- It does not read any values from the DOM (no `byId()` calls found)
+- It does not access any global configuration variables
+- All necessary data is passed through constructor parameters:
+ - `delaunay`: Delaunator instance
+ - `points`: Array of coordinate pairs
+ - `pointsN`: Number of points
+
+## No Configuration Needed:
+Since this module performs purely geometric calculations based on input data and does not interact with UI elements or global state, no `config` object is required for this refactoring.
\ No newline at end of file
diff --git a/procedural/src/engine/support/voronoi_external.md b/procedural/src/engine/support/voronoi_external.md
new file mode 100644
index 00000000..895a0c7d
--- /dev/null
+++ b/procedural/src/engine/support/voronoi_external.md
@@ -0,0 +1,20 @@
+# External Dependencies for voronoi.js
+
+The refactored `voronoi.js` module has **no external dependencies** beyond standard JavaScript.
+
+## Analysis:
+- The Voronoi class is a pure computational module that works with geometric algorithms
+- It only depends on:
+ - Standard JavaScript Math functions
+ - Array methods (map, filter)
+ - Basic data structures (arrays, objects)
+- No imports from other modules are required
+- The Delaunator instance is passed as a constructor parameter, not imported
+
+## Constructor Dependencies:
+The Voronoi class expects to receive:
+- `delaunay`: A Delaunator instance (passed from calling code)
+- `points`: Array of coordinate pairs
+- `pointsN`: Number of points
+
+These are injected dependencies, not module imports.
\ No newline at end of file
diff --git a/procedural/src/engine/support/voronoi_prompt.md b/procedural/src/engine/support/voronoi_prompt.md
new file mode 100644
index 00000000..e9235573
--- /dev/null
+++ b/procedural/src/engine/support/voronoi_prompt.md
@@ -0,0 +1,217 @@
+# voronoi.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+7. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+8. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `voronoi.js`.
+
+**File Content:**
+```javascript
+class Voronoi {
+ /**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+ constructor(delaunay, points, pointsN) {
+ this.delaunay = delaunay;
+ this.points = points;
+ this.pointsN = pointsN;
+ this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
+ this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+
+ // Half-edges are the indices into the delaunator outputs:
+ // delaunay.triangles[e] gives the point ID where the half-edge starts
+ // delaunay.halfedges[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
+ for (let e = 0; e < this.delaunay.triangles.length; e++) {
+
+ const p = this.delaunay.triangles[this.nextHalfedge(e)];
+ if (p < this.pointsN && !this.cells.c[p]) {
+ const edges = this.edgesAroundPoint(e);
+ this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
+ this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
+ this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
+ }
+
+ const t = this.triangleOfEdge(e);
+ if (!this.vertices.p[t]) {
+ this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
+ this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
+ this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
+ }
+ }
+ }
+
+ /**
+ * Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The IDs of the points comprising the given triangle.
+ */
+ pointsOfTriangle(t) {
+ return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ }
+
+ /**
+ * Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {number[]} The indices of the triangles that share half-edges with this triangle.
+ */
+ trianglesAdjacentToTriangle(t) {
+ let triangles = [];
+ for (let edge of this.edgesOfTriangle(t)) {
+ let opposite = this.delaunay.halfedges[edge];
+ triangles.push(this.triangleOfEdge(opposite));
+ }
+ return triangles;
+ }
+
+ /**
+ * Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
+ * @param {number} start The index of an incoming half-edge that leads to the desired point
+ * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ */
+ edgesAroundPoint(start) {
+ const result = [];
+ let incoming = start;
+ do {
+ result.push(incoming);
+ const outgoing = this.nextHalfedge(incoming);
+ incoming = this.delaunay.halfedges[outgoing];
+ } while (incoming !== -1 && incoming !== start && result.length < 20);
+ return result;
+ }
+
+ /**
+ * Returns the center of the triangle located at the given index.
+ * @param {number} t The index of the triangle
+ * @returns {[number, number]}
+ */
+ triangleCenter(t) {
+ let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ return this.circumcenter(vertices[0], vertices[1], vertices[2]);
+ }
+
+ /**
+ * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The edges of the triangle.
+ */
+ edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+
+ /**
+ * Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} e The index of the edge
+ * @returns {number} The index of the triangle
+ */
+ triangleOfEdge(e) { return Math.floor(e / 3); }
+
+ /**
+ * Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the next half edge
+ */
+ nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+
+ /**
+ * Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the previous half edge
+ */
+ prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+
+ /**
+ * Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
+ * @param {[number, number]} a The coordinates of the first point of the triangle
+ * @param {[number, number]} b The coordinates of the second point of the triangle
+ * @param {[number, number]} c The coordinates of the third point of the triangle
+ * @return {[number, number]} The coordinates of the circumcenter of the triangle.
+ */
+ circumcenter(a, b, c) {
+ const [ax, ay] = a;
+ const [bx, by] = b;
+ const [cx, cy] = c;
+ const ad = ax * ax + ay * ay;
+ const bd = bx * bx + by * by;
+ const cd = cx * cx + cy * cy;
+ const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
+ return [
+ Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
+ Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
+ ];
+ }
+}
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./voronoi.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./voronoi_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in voronoi_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into voronoi_render.md
diff --git a/procedural/src/engine/support/zones-generator_prompt.md b/procedural/src/engine/support/zones-generator_prompt.md
new file mode 100644
index 00000000..63606786
--- /dev/null
+++ b/procedural/src/engine/support/zones-generator_prompt.md
@@ -0,0 +1,538 @@
+# module_name.js
+
+**You are an expert senior JavaScript developer specializing in refactoring legacy code into modern, modular, and environment-agnostic libraries. You have a deep understanding of design patterns like dependency injection and the separation of concerns.**
+
+**Your Goal:**
+
+Your task is to refactor a single JavaScript module from a legacy Fantasy Map Generator application. The goal is to migrate it from its old, browser-dependent format into a pure, headless-first ES module that will be part of a core generation engine. This engine must be able to run in any JavaScript environment, including Node.js, without any dependencies on a browser or DOM.
+
+**Architectural Context:**
+
+* **Old Architecture:** The original code is wrapped in an IIFE and attaches its exports to the global `window` object. It directly reads from and mutates global state variables like `pack` and `grid`, and directly accesses the DOM via `byId()`.
+* **New Architecture (Target):**
+ 1. **Core Engine:** A collection of pure ES modules. It receives all necessary data (`pack`, `grid`) and configuration as function arguments. It performs its logic and returns the newly generated data. It has **zero** knowledge of the browser.
+ 2. **Viewer/Client:** The application responsible for all DOM interaction, UI, and rendering SVG based on the data object produced by the engine.
+
+**The Golden Rules of Refactoring for the Core Engine:**
+
+1. **No Globals:** Remove the IIFE and the attachment to the `window` object.
+2. **Use ES Modules:** All exported functions and data must use the `export` keyword.
+3. **Dependency Injection:** Functions must not read from or mutate global state. All data they need (`pack`, `grid`) must be passed in as arguments.
+4. **Introduce a `config` Object:**
+ * **When you find code that reads a value from the DOM (e.g., `byId("statesNumber").value`), this is a configuration parameter.**
+ * **You must replace this DOM call with a property from a `config` object (e.g., `config.statesNumber`).**
+ * Add this `config` object as a new argument to the function's signature.
+5. **Return New Data:** Instead of modifying an object in place (e.g., `pack.cells.biome = ...`), functions should create the new data and return it. The calling function will be responsible for merging this data into the main state object.
+6. **Pure functions:** Functions should not have side effects. They should either return a new state object or a specific piece of data.
+7. **Strict Separation of Concerns (Crucial):**
+ * **UI Input Reading:** As per Rule #4, these `byId()` calls are your guide to what properties the `config` object needs.
+ * **Rendering Logic:** Any code that **writes to the DOM or SVG** (e.g., `d3.select`, `document.getElementById(...).innerHTML = ...`, creating `` elements, etc.) is considered rendering logic.
+ * **You must REMOVE all rendering logic** from the engine module.
+8. **Maintain Style:** Preserve the original code style, comments, and variable names as much as possible for consistency.
+9. **Efficient Destructuring:** When passing a utils object, only destructure the specific properties needed within the scope of the function that uses them, rather than destructuring the entire object at the top of every function. This improves clarity and reduces code repetition.
+
+---
+
+**Concrete Example of Refactoring:**
+
+**BEFORE (Legacy `burgs-and-states.js`):**
+
+```javascript
+// ...
+function placeCapitals() {
+ // Direct DOM read - THIS IS A CONFIGURATION VALUE
+ let count = +byId("statesNumber").value;
+ // ...
+}
+// ...
+```
+
+**AFTER (Refactored `engine/modules/burgsAndStates.js`):**
+
+```javascript
+// ...
+// Dependencies, including the new `config` object, are injected.
+export function placeCapitals(cells, graphWidth, graphHeight, config) {
+ // DOM read is replaced by a property from the `config` object.
+ let count = config.statesNumber;
+ // ...
+ // Returns the generated data
+ return { burgs, states };
+}
+// ...
+```
+
+---
+
+**Your Specific Task:**
+
+Now, please apply these principles to refactor the following module: `module_name.js`.
+
+**File Content:**
+```javascript
+"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};
+})();
+
+```
+
+**Instructions:**
+
+Provide a response in three parts:
+
+1. **Refactored Code:** The complete JavaScript code for the new ES module in ./module_name.js
+2. **Engine Dependencies:**
+ * List the external modules the refactored code will need to `import` (e.g., `Names`, `COA`) in ./module_name_external.md
+ * **List the new `config` properties you identified and used** (e.g., `statesNumber`, `growthRate`) in module_name_config.md This is essential.
+3. **Removed Rendering/UI Logic:** List all the code blocks related to DOM manipulation or SVG rendering that you have **removed** so they can be moved to the Viewer application into module_name_render.md
diff --git a/procedural/src/engine/support/zones_config.md b/procedural/src/engine/support/zones_config.md
new file mode 100644
index 00000000..8b462ade
--- /dev/null
+++ b/procedural/src/engine/support/zones_config.md
@@ -0,0 +1,24 @@
+# Configuration Properties for zones.js
+
+The refactored zones.js module accepts the following configuration properties:
+
+## Configuration Object Properties
+
+### `globalModifier` (optional)
+- **Type**: Number
+- **Default**: 1
+- **Description**: Global modifier for zone generation quantity. Multiplies the base quantity of each zone type.
+- **Usage**: Controls overall density of zones generated across the map
+
+### `TIME` (optional)
+- **Type**: Boolean
+- **Default**: false
+- **Description**: Debug flag to enable/disable console timing for performance measurement
+- **Usage**: When true, logs execution time of zone generation to console
+
+## Notes
+
+- No DOM-dependent configuration properties were found in the original code
+- The original module did not read any values from DOM elements via `byId()`
+- All zone generation parameters are internally configured in the `zoneConfig` object
+- The module is purely data-driven and does not require UI input for configuration
\ No newline at end of file
diff --git a/procedural/src/engine/support/zones_external.md b/procedural/src/engine/support/zones_external.md
new file mode 100644
index 00000000..433a9a9f
--- /dev/null
+++ b/procedural/src/engine/support/zones_external.md
@@ -0,0 +1,22 @@
+# External Dependencies for zones.js
+
+The refactored zones.js module requires the following external dependencies to be imported:
+
+## Utility Functions (utils.random)
+- `gauss` - Gaussian random number generator
+- `ra` - Random array element selector
+- `rw` - Weighted random selector
+- `P` - Probability function
+- `rand` - Random number generator with min/max
+- `getAdjective` - Function to get adjective form of names
+
+## Data Structures and Libraries (utils)
+- `Names` - Name generation utilities (specifically `Names.getCultureShort()`)
+- `Routes` - Route utilities (`Routes.getRoute()`, `Routes.isConnected()`)
+- `FlatQueue` - Priority queue data structure for pathfinding
+- `d3` - D3.js library (specifically `d3.mean()`, `d3.max()`)
+
+## Global Dependencies (passed as parameters)
+- `pack` - Main game data object containing cells, states, religions, burgs, markers, features
+- `notes` - Array of notes objects (used in eruption generation)
+- `config` - Configuration object for runtime settings
\ No newline at end of file
diff --git a/procedural/src/engine/support/zones_render.md b/procedural/src/engine/support/zones_render.md
new file mode 100644
index 00000000..8c2deb66
--- /dev/null
+++ b/procedural/src/engine/support/zones_render.md
@@ -0,0 +1,27 @@
+# Removed Rendering/UI Logic from zones.js
+
+## Analysis Result
+
+**No rendering or UI logic was found in the original zones.js module.**
+
+The original code was purely focused on data generation and did not contain any:
+
+- DOM manipulation code
+- SVG rendering logic
+- `d3.select()` calls for rendering
+- `document.getElementById()` or similar DOM access
+- Creation of HTML/SVG elements
+- Direct UI updates or modifications
+
+## Code Characteristics
+
+The zones.js module was already well-separated in terms of concerns:
+
+- **Data Generation Only**: The module exclusively generates zone data structures
+- **No DOM Dependencies**: No direct browser/DOM dependencies were present
+- **Pure Logic**: All functions perform calculations and return data objects
+- **No Side Effects**: Functions don't modify DOM or trigger rendering
+
+## Conclusion
+
+Since no rendering logic was present in the original module, no code blocks needed to be removed for the Viewer application. The module was already appropriately focused on its core responsibility of generating zone data.
\ No newline at end of file
diff --git a/procedural/src/engine/utils/alea.js b/procedural/src/engine/utils/alea.js
new file mode 100644
index 00000000..5b12142c
--- /dev/null
+++ b/procedural/src/engine/utils/alea.js
@@ -0,0 +1,4 @@
+/*https://github.com/macmcmeans/aleaPRNG/blob/master/aleaPRNG-1.1.js
+©2010 Johannes Baagøe, MIT license; Derivative ©2017-2020 W. Mac" McMeans, BSD license.*/
+const aleaPRNG=function(){return function(n){"use strict";var r,t,e,o,a,u=new Uint32Array(3),i="";function c(n){var a=function(){var n=4022871197,r=function(r){r=r.toString();for(var t=0,e=r.length;t>>0,n=(o*=n)>>>0,n+=4294967296*(o-=n)}return 2.3283064365386963e-10*(n>>>0)};return r.version="Mash 0.9",r}();r=a(" "),t=a(" "),e=a(" "),o=1;for(var u=0;uarguments[1]&&(n=arguments[1],r=arguments[0]),f(n)&&f(r)?Math.floor(l()*(r-n+1))+n:l()*(r-n)+n},l.restart=function(){c(a)},l.seed=function(){c(Array.prototype.slice.call(arguments))},l.version=function(){return"aleaPRNG 1.1.0"},l.versions=function(){return"aleaPRNG 1.1.0, "+i},0===n.length&&(window.crypto.getRandomValues(u),n=[u[0],u[1],u[2]]),a=n,c(n),l}(Array.prototype.slice.call(arguments))};
+export { aleaPRNG };
\ No newline at end of file
diff --git a/procedural/src/engine/utils/arrayUtils.js b/procedural/src/engine/utils/arrayUtils.js
new file mode 100644
index 00000000..d2b840ad
--- /dev/null
+++ b/procedural/src/engine/utils/arrayUtils.js
@@ -0,0 +1,62 @@
+"use strict";
+
+function last(array) {
+ return array[array.length - 1];
+}
+
+function unique(array) {
+ return [...new Set(array)];
+}
+
+// deep copy for Arrays (and other objects)
+function deepCopy(obj) {
+ const id = x => x;
+ const dcTArray = a => a.map(id);
+ const dcObject = x => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
+ const dcAny = x => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
+ // don't map keys, probably this is what we would expect
+ const dcMapCore = m => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
+
+ const cf = new Map([
+ [Int8Array, dcTArray],
+ [Uint8Array, dcTArray],
+ [Uint8ClampedArray, dcTArray],
+ [Int16Array, dcTArray],
+ [Uint16Array, dcTArray],
+ [Int32Array, dcTArray],
+ [Uint32Array, dcTArray],
+ [Float32Array, dcTArray],
+ [Float64Array, dcTArray],
+ [BigInt64Array, dcTArray],
+ [BigUint64Array, dcTArray],
+ [Map, m => new Map(dcMapCore(m))],
+ [WeakMap, m => new WeakMap(dcMapCore(m))],
+ [Array, a => a.map(dcAny)],
+ [Set, s => [...s.values()].map(dcAny)],
+ [Date, d => new Date(d.getTime())],
+ [Object, dcObject]
+ // ... extend here to implement their custom deep copy
+ ]);
+
+ return dcAny(obj);
+}
+
+function getTypedArray(maxValue) {
+ console.assert(
+ Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= UINT32_MAX,
+ `Array maxValue must be an integer between 0 and ${UINT32_MAX}, got ${maxValue}`
+ );
+
+ if (maxValue <= UINT8_MAX) return Uint8Array;
+ if (maxValue <= UINT16_MAX) return Uint16Array;
+ if (maxValue <= UINT32_MAX) return Uint32Array;
+ return Uint32Array;
+}
+
+function createTypedArray({maxValue, length, from}) {
+ const typedArray = getTypedArray(maxValue);
+ if (!from) return new typedArray(length);
+ return typedArray.from(from);
+}
+
+export { last, unique, deepCopy, getTypedArray, createTypedArray };
diff --git a/procedural/src/engine/utils/cell.js b/procedural/src/engine/utils/cell.js
new file mode 100644
index 00000000..38dd155c
--- /dev/null
+++ b/procedural/src/engine/utils/cell.js
@@ -0,0 +1,54 @@
+"use strict";
+// FMG utils related to cell ranking and population
+
+// calculate cell suitability and population based on various factors
+function rankCells(pack, grid, utils, modules) {
+ const { TIME, normalize } = utils;
+ const { biomesData } = modules;
+
+ TIME && console.time("rankCells");
+
+ const { cells, features } = pack;
+ const s = new Int16Array(cells.i.length); // cell suitability array
+ const pop = new Float32Array(cells.i.length); // cell population array
+
+ const flMean = utils.d3.median(cells.fl.filter(f => f)) || 0;
+ const flMax = utils.d3.max(cells.fl) + utils.d3.max(cells.conf); // to normalize flux
+ const areaMean = utils.d3.mean(cells.area); // to adjust population by cell area
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue; // no population in water
+
+ let suitability = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
+ if (!suitability) continue; // uninhabitable biomes has 0 suitability
+
+ if (flMean) suitability += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued
+ suitability -= (cells.h[i] - 50) / 5; // low elevation is valued, high is not;
+
+ if (cells.t[i] === 1) {
+ if (cells.r[i]) suitability += 15; // estuary is valued
+ const feature = features[cells.f[cells.haven[i]]];
+ if (feature.type === "lake") {
+ if (feature.group === "freshwater") suitability += 30;
+ else if (feature.group == "salt") suitability += 10;
+ else if (feature.group == "frozen") suitability += 1;
+ else if (feature.group == "dry") suitability -= 5;
+ else if (feature.group == "sinkhole") suitability -= 5;
+ else if (feature.group == "lava") suitability -= 30;
+ } else {
+ suitability += 5; // ocean coast is valued
+ if (cells.harbor[i] === 1) suitability += 20; // safe sea harbor is valued
+ }
+ }
+
+ s[i] = suitability / 5; // general population rate
+ // cell rural population is suitability adjusted by cell area
+ pop[i] = s[i] > 0 ? (s[i] * cells.area[i]) / areaMean : 0;
+ }
+
+ TIME && console.timeEnd("rankCells");
+
+ return { s, pop };
+}
+
+export { rankCells };
\ No newline at end of file
diff --git a/procedural/src/engine/utils/colorUtils.js b/procedural/src/engine/utils/colorUtils.js
new file mode 100644
index 00000000..2de0d312
--- /dev/null
+++ b/procedural/src/engine/utils/colorUtils.js
@@ -0,0 +1,51 @@
+"use strict";
+// FMG utils related to colors
+
+// convert RGB color string to HEX without #
+function toHEX(rgb) {
+ if (rgb.charAt(0) === "#") return rgb;
+
+ rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
+ return rgb && rgb.length === 4
+ ? "#" +
+ ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2)
+ : "";
+}
+
+const C_12 = [
+ "#dababf",
+ "#fb8072",
+ "#80b1d3",
+ "#fdb462",
+ "#b3de69",
+ "#fccde5",
+ "#c6b9c1",
+ "#bc80bd",
+ "#ccebc5",
+ "#ffed6f",
+ "#8dd3c7",
+ "#eb8de7"
+];
+const scaleRainbow = d3.scaleSequential(d3.interpolateRainbow);
+
+// return array of standard shuffled colors
+function getColors(number) {
+ const colors = d3.shuffle(
+ d3.range(number).map(i => (i < 12 ? C_12[i] : d3.color(scaleRainbow((i - 12) / (number - 12))).hex()))
+ );
+ return colors;
+}
+
+function getRandomColor() {
+ return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
+}
+
+// mix a color with a random color
+function getMixedColor(color, mix = 0.2, bright = 0.3) {
+ const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
+ return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
+}
+
+export { toHEX, getColors, getRandomColor, getMixedColor };
diff --git a/procedural/src/engine/utils/commonUtils.js b/procedural/src/engine/utils/commonUtils.js
new file mode 100644
index 00000000..e0b69229
--- /dev/null
+++ b/procedural/src/engine/utils/commonUtils.js
@@ -0,0 +1,195 @@
+"use strict";
+// FMG helper functions
+
+// clip polygon by graph bbox
+function clipPoly(points, secure = 0) {
+ if (points.length < 2) return points;
+ if (points.some(point => point === undefined)) {
+ ERROR && console.error("Undefined point in clipPoly", points);
+ return points;
+ }
+
+ return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
+}
+
+// get segment of any point on polyline
+function getSegmentId(points, point, step = 10) {
+ if (points.length === 2) return 1;
+
+ let minSegment = 1;
+ let minDist = Infinity;
+
+ for (let i = 0; i < points.length - 1; i++) {
+ const p1 = points[i];
+ const p2 = points[i + 1];
+
+ const length = Math.sqrt(dist2(p1, p2));
+ const segments = Math.ceil(length / step);
+ const dx = (p2[0] - p1[0]) / segments;
+ const dy = (p2[1] - p1[1]) / segments;
+
+ for (let s = 0; s < segments; s++) {
+ const x = p1[0] + s * dx;
+ const y = p1[1] + s * dy;
+ const dist = dist2(point, [x, y]);
+
+ if (dist >= minDist) continue;
+ minDist = dist;
+ minSegment = i + 1;
+ }
+ }
+
+ return minSegment;
+}
+
+function debounce(func, ms) {
+ let isCooldown = false;
+
+ return function () {
+ if (isCooldown) return;
+ func.apply(this, arguments);
+ isCooldown = true;
+ setTimeout(() => (isCooldown = false), ms);
+ };
+}
+
+function throttle(func, ms) {
+ let isThrottled = false;
+ let savedArgs;
+ let savedThis;
+
+ function wrapper() {
+ if (isThrottled) {
+ savedArgs = arguments;
+ savedThis = this;
+ return;
+ }
+
+ func.apply(this, arguments);
+ isThrottled = true;
+
+ setTimeout(function () {
+ isThrottled = false;
+ if (savedArgs) {
+ wrapper.apply(savedThis, savedArgs);
+ savedArgs = savedThis = null;
+ }
+ }, ms);
+ }
+
+ return wrapper;
+}
+
+// parse error to get the readable string in Chrome and Firefox
+function parseError(error) {
+ const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
+ const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
+ const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
+ const errorNoURL = errorString.replace(regex, url => "" + last(url.split("/")) + " ");
+ const errorParsed = errorNoURL.replace(/at /gi, " at ");
+ return errorParsed;
+}
+
+function getBase64(url, callback) {
+ const xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ callback(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open("GET", url);
+ xhr.responseType = "blob";
+ xhr.send();
+}
+
+// open URL in a new tab or window
+function openURL(url) {
+ window.open(url, "_blank");
+}
+
+// open project wiki-page
+function wiki(page) {
+ window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
+}
+
+// wrap URL into html a element
+function link(URL, description) {
+ return `${description} `;
+}
+
+function isCtrlClick(event) {
+ // meta key is cmd key on MacOs
+ return event.ctrlKey || event.metaKey;
+}
+
+function generateDate(from = 100, to = 1000) {
+ return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
+ year: "numeric",
+ month: "long",
+ day: "numeric"
+ });
+}
+
+function getLongitude(x, decimals = 2) {
+ return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
+}
+
+function getLatitude(y, decimals = 2) {
+ return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
+}
+
+function getCoordinates(x, y, decimals = 2) {
+ return [getLongitude(x, decimals), getLatitude(y, decimals)];
+}
+
+// prompt replacer (prompt does not work in Electron)
+void (function () {
+ const prompt = document.getElementById("prompt");
+ const form = prompt.querySelector("#promptForm");
+
+ const defaultText = "Please provide an input";
+ const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
+
+ window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
+ if (options.default === undefined)
+ return ERROR && console.error("Prompt: options object does not have default value defined");
+
+ const input = prompt.querySelector("#promptInput");
+ prompt.querySelector("#promptText").innerHTML = promptText;
+
+ const type = typeof options.default === "number" ? "number" : "text";
+ input.type = type;
+
+ if (options.step !== undefined) input.step = options.step;
+ if (options.min !== undefined) input.min = options.min;
+ if (options.max !== undefined) input.max = options.max;
+
+ input.required = options.required === false ? false : true;
+ input.placeholder = "type a " + type;
+ input.value = options.default;
+ prompt.style.display = "block";
+
+ form.addEventListener(
+ "submit",
+ event => {
+ event.preventDefault();
+ prompt.style.display = "none";
+ const v = type === "number" ? +input.value : input.value;
+ if (callback) callback(v);
+ },
+ {once: true}
+ );
+ };
+
+ const cancel = prompt.querySelector("#promptCancel");
+ cancel.addEventListener("click", () => {
+ prompt.style.display = "none";
+ });
+})();
+
+export {
+ clipPoly, getSegmentId, debounce, throttle, parseError, getBase64, openURL,
+ wiki, link, isCtrlClick, generateDate, getLongitude, getLatitude, getCoordinates
+};
diff --git a/procedural/src/engine/utils/debugUtils.js b/procedural/src/engine/utils/debugUtils.js
new file mode 100644
index 00000000..5959cf71
--- /dev/null
+++ b/procedural/src/engine/utils/debugUtils.js
@@ -0,0 +1,74 @@
+"use strict";
+// FMG utils used for debugging
+
+function drawCellsValue(data) {
+ debug.selectAll("text").remove();
+ debug
+ .selectAll("text")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("x", (d, i) => pack.cells.p[i][0])
+ .attr("y", (d, i) => pack.cells.p[i][1])
+ .text(d => d);
+}
+
+function drawPolygons(data) {
+ const max = d3.max(data);
+ const min = d3.min(data);
+ const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
+
+ data = data.map(d => 1 - normalize(d, min, max));
+
+ debug.selectAll("polygon").remove();
+ debug
+ .selectAll("polygon")
+ .data(data)
+ .enter()
+ .append("polygon")
+ .attr("points", (d, i) => getGridPolygon(i))
+ .attr("fill", d => scheme(d))
+ .attr("stroke", d => scheme(d));
+}
+
+function drawRouteConnections() {
+ debug.select("#connections").remove();
+ const routes = debug.append("g").attr("id", "connections").attr("stroke-width", 0.8);
+
+ const points = pack.cells.p;
+ const links = pack.cells.routes;
+
+ for (const from in links) {
+ for (const to in links[from]) {
+ const [x1, y1] = points[from];
+ const [x3, y3] = points[to];
+ const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
+ const routeId = links[from][to];
+
+ routes
+ .append("line")
+ .attr("x1", x1)
+ .attr("y1", y1)
+ .attr("x2", x2)
+ .attr("y2", y2)
+ .attr("data-id", routeId)
+ .attr("stroke", C_12[routeId % 12]);
+ }
+ }
+}
+
+function drawPoint([x, y], {color = "red", radius = 0.5}) {
+ debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
+}
+
+function drawPath(points, {color = "red", width = 0.5}) {
+ const lineGen = d3.line().curve(d3.curveBundle);
+ debug
+ .append("path")
+ .attr("d", round(lineGen(points)))
+ .attr("stroke", color)
+ .attr("stroke-width", width)
+ .attr("fill", "none");
+}
+
+export { drawCellsValue, drawPolygons, drawRouteConnections, drawPoint, drawPath };
diff --git a/procedural/src/engine/utils/flatqueue.js b/procedural/src/engine/utils/flatqueue.js
new file mode 100644
index 00000000..271aaf32
--- /dev/null
+++ b/procedural/src/engine/utils/flatqueue.js
@@ -0,0 +1,57 @@
+!(function (t, s) {
+ "object" == typeof exports && "undefined" != typeof module
+ ? (module.exports = s())
+ : "function" == typeof define && define.amd
+ ? define(s)
+ : ((t = "undefined" != typeof globalThis ? globalThis : t || self).FlatQueue = s());
+})(this, function () {
+ "use strict";
+ return class {
+ constructor() {
+ (this.ids = []), (this.values = []), (this.length = 0);
+ }
+ clear() {
+ this.length = 0;
+ }
+ push(t, s) {
+ let i = this.length++;
+ for (; i > 0; ) {
+ const t = (i - 1) >> 1,
+ e = this.values[t];
+ if (s >= e) break;
+ (this.ids[i] = this.ids[t]), (this.values[i] = e), (i = t);
+ }
+ (this.ids[i] = t), (this.values[i] = s);
+ }
+ pop() {
+ if (0 === this.length) return;
+ const t = this.ids[0];
+ if ((this.length--, this.length > 0)) {
+ const t = (this.ids[0] = this.ids[this.length]),
+ s = (this.values[0] = this.values[this.length]),
+ i = this.length >> 1;
+ let e = 0;
+ for (; e < i; ) {
+ let t = 1 + (e << 1);
+ const i = t + 1;
+ let h = this.ids[t],
+ l = this.values[t];
+ const n = this.values[i];
+ if ((i < this.length && n < l && ((t = i), (h = this.ids[i]), (l = n)), l >= s)) break;
+ (this.ids[e] = h), (this.values[e] = l), (e = t);
+ }
+ (this.ids[e] = t), (this.values[e] = s);
+ }
+ return t;
+ }
+ peek() {
+ if (0 !== this.length) return this.ids[0];
+ }
+ peekValue() {
+ if (0 !== this.length) return this.values[0];
+ }
+ shrink() {
+ this.ids.length = this.values.length = this.length;
+ }
+ };
+});
diff --git a/procedural/src/engine/utils/functionUtils.js b/procedural/src/engine/utils/functionUtils.js
new file mode 100644
index 00000000..0e0da03c
--- /dev/null
+++ b/procedural/src/engine/utils/functionUtils.js
@@ -0,0 +1,33 @@
+"use strict";
+// FMG helper functions
+
+// extracted d3 code to bypass version conflicts
+// https://github.com/d3/d3-array/blob/main/src/group.js
+function rollups(values, reduce, ...keys) {
+ return nest(values, Array.from, reduce, keys);
+}
+
+function nest(values, map, reduce, keys) {
+ return (function regroup(values, i) {
+ if (i >= keys.length) return reduce(values);
+ const groups = new Map();
+ const keyof = keys[i++];
+ let index = -1;
+ for (const value of values) {
+ const key = keyof(value, ++index, values);
+ const group = groups.get(key);
+ if (group) group.push(value);
+ else groups.set(key, [value]);
+ }
+ for (const [key, values] of groups) {
+ groups.set(key, regroup(values, i));
+ }
+ return map(groups);
+ })(values, 0);
+}
+
+function dist2([x1, y1], [x2, y2]) {
+ return (x1 - x2) ** 2 + (y1 - y2) ** 2;
+}
+
+export { rollups, nest, dist2 };
diff --git a/procedural/src/engine/utils/geography.js b/procedural/src/engine/utils/geography.js
new file mode 100644
index 00000000..53f50ab7
--- /dev/null
+++ b/procedural/src/engine/utils/geography.js
@@ -0,0 +1,367 @@
+"use strict";
+// FMG utils related to geography and climate
+
+// add lakes in cells that are too deep and cannot pour to sea
+function addLakesInDeepDepressions(grid, config, utils) {
+ const { TIME } = utils;
+ TIME && console.time("addLakesInDeepDepressions");
+
+ const elevationLimit = config.elevationLimit || 80;
+ if (elevationLimit === 80) return grid;
+
+ const { cells, features } = grid;
+ const { c, h, b } = cells;
+
+ for (const i of cells.i) {
+ if (b[i] || h[i] < 20) continue;
+
+ const minHeight = Math.min(...c[i].map(c => h[c]));
+ if (h[i] > minHeight) continue;
+
+ let deep = true;
+ const threshold = h[i] + elevationLimit;
+ const queue = [i];
+ const checked = [];
+ checked[i] = true;
+
+ // check if elevated cell can potentially pour to water
+ while (deep && queue.length) {
+ const q = queue.pop();
+
+ for (const n of c[q]) {
+ if (checked[n]) continue;
+ if (h[n] >= threshold) continue;
+ if (h[n] < 20) {
+ deep = false;
+ break;
+ }
+
+ checked[n] = true;
+ queue.push(n);
+ }
+ }
+
+ // if not, add a lake
+ if (deep) {
+ const lakeCells = [i].concat(c[i].filter(n => h[n] === h[i]));
+ addLake(lakeCells);
+ }
+ }
+
+ function addLake(lakeCells) {
+ const f = features.length;
+
+ lakeCells.forEach(i => {
+ cells.h[i] = 19;
+ cells.t[i] = -1;
+ cells.f[i] = f;
+ c[i].forEach(n => !lakeCells.includes(n) && (cells.t[n] = 1));
+ });
+
+ features.push({ i: f, land: false, border: false, type: "lake" });
+ }
+
+ TIME && console.timeEnd("addLakesInDeepDepressions");
+ return grid;
+}
+
+// open near-sea lakes by removing shallow elevation barriers
+function openNearSeaLakes(grid, config, utils) {
+ const { TIME } = utils;
+ const template = config.template;
+ if (template === "Atoll") return grid; // no need for Atolls
+
+ const { cells, features } = grid;
+ if (!features.find(f => f.type === "lake")) return grid; // no lakes
+
+ TIME && console.time("openLakes");
+ const LIMIT = config.openLakeLimit || 22; // max height that can be breached by water
+
+ for (const i of cells.i) {
+ const lakeFeatureId = cells.f[i];
+ if (features[lakeFeatureId].type !== "lake") continue; // not a lake
+
+ check_neighbours: for (const c of cells.c[i]) {
+ if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot break this
+
+ for (const n of cells.c[c]) {
+ const ocean = cells.f[n];
+ if (features[ocean].type !== "ocean") continue; // not an ocean
+ removeLake(c, lakeFeatureId, ocean);
+ break check_neighbours;
+ }
+ }
+ }
+
+ function removeLake(thresholdCellId, lakeFeatureId, oceanFeatureId) {
+ cells.h[thresholdCellId] = 19;
+ cells.t[thresholdCellId] = -1;
+ cells.f[thresholdCellId] = oceanFeatureId;
+ cells.c[thresholdCellId].forEach(function (c) {
+ if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline
+ });
+
+ cells.i.forEach(i => {
+ if (cells.f[i] === lakeFeatureId) cells.f[i] = oceanFeatureId;
+ });
+ features[lakeFeatureId].type = "ocean"; // mark former lake as ocean
+ }
+
+ TIME && console.timeEnd("openLakes");
+ return grid;
+}
+
+// define map size and coordinate system based on template
+function defineMapSize(grid, config) {
+ const [size, latitude, longitude] = getSizeAndLatitude(config.template, grid);
+
+ return {
+ mapCoordinates: calculateMapCoordinates(size, latitude, longitude, config.graphWidth, config.graphHeight),
+ size,
+ latitude,
+ longitude
+ };
+
+ function getSizeAndLatitude(template, grid) {
+ const { rn, gauss, P } = config.utils || {};
+
+ if (template === "africa-centric") return [45, 53, 38];
+ if (template === "arabia") return [20, 35, 35];
+ if (template === "atlantics") return [42, 23, 65];
+ if (template === "britain") return [7, 20, 51.3];
+ if (template === "caribbean") return [15, 40, 74.8];
+ if (template === "east-asia") return [11, 28, 9.4];
+ if (template === "eurasia") return [38, 19, 27];
+ if (template === "europe") return [20, 16, 44.8];
+ if (template === "europe-accented") return [14, 22, 44.8];
+ if (template === "europe-and-central-asia") return [25, 10, 39.5];
+ if (template === "europe-central") return [11, 22, 46.4];
+ if (template === "europe-north") return [7, 18, 48.9];
+ if (template === "greenland") return [22, 7, 55.8];
+ if (template === "hellenica") return [8, 27, 43.5];
+ if (template === "iceland") return [2, 15, 55.3];
+ if (template === "indian-ocean") return [45, 55, 14];
+ if (template === "mediterranean-sea") return [10, 29, 45.8];
+ if (template === "middle-east") return [8, 31, 34.4];
+ if (template === "north-america") return [37, 17, 87];
+ if (template === "us-centric") return [66, 27, 100];
+ if (template === "us-mainland") return [16, 30, 77.5];
+ if (template === "world") return [78, 27, 40];
+ if (template === "world-from-pacific") return [75, 32, 30];
+
+ const part = grid.features.some(f => f.land && f.border); // if land goes over map borders
+ const max = part ? 80 : 100; // max size
+ const lat = () => gauss(P(0.5) ? 40 : 60, 20, 25, 75); // latitude shift
+
+ if (!part) {
+ if (template === "pangea") return [100, 50, 50];
+ if (template === "shattered" && P(0.7)) return [100, 50, 50];
+ if (template === "continents" && P(0.5)) return [100, 50, 50];
+ if (template === "archipelago" && P(0.35)) return [100, 50, 50];
+ if (template === "highIsland" && P(0.25)) return [100, 50, 50];
+ if (template === "lowIsland" && P(0.1)) return [100, 50, 50];
+ }
+
+ if (template === "pangea") return [gauss(70, 20, 30, max), lat(), 50];
+ if (template === "volcano") return [gauss(20, 20, 10, max), lat(), 50];
+ if (template === "mediterranean") return [gauss(25, 30, 15, 80), lat(), 50];
+ if (template === "peninsula") return [gauss(15, 15, 5, 80), lat(), 50];
+ if (template === "isthmus") return [gauss(15, 20, 3, 80), lat(), 50];
+ if (template === "atoll") return [gauss(3, 2, 1, 5, 1), lat(), 50];
+
+ return [gauss(30, 20, 15, max), lat(), 50]; // Continents, Archipelago, High Island, Low Island
+ }
+}
+
+// calculate map coordinates from size and position parameters
+function calculateMapCoordinates(sizeFraction, latShift, lonShift, graphWidth, graphHeight, utils) {
+ const { rn } = utils;
+
+ const latT = rn(sizeFraction * 180 / 100, 1);
+ const latN = rn(90 - (180 - latT) * latShift / 100, 1);
+ const latS = rn(latN - latT, 1);
+
+ const lonT = rn(Math.min((graphWidth / graphHeight) * latT, 360), 1);
+ const lonE = rn(180 - (360 - lonT) * lonShift / 100, 1);
+ const lonW = rn(lonE - lonT, 1);
+
+ return { latT, latN, latS, lonT, lonW, lonE };
+}
+
+// calculate temperatures based on latitude and elevation
+function calculateTemperatures(grid, mapCoordinates, config, utils) {
+ const { TIME, rn, minmax } = utils;
+ TIME && console.time("calculateTemperatures");
+
+ const { cells } = grid;
+ const temp = new Int8Array(cells.i.length); // temperature array
+
+ const { temperatureEquator = 30, temperatureNorthPole = -10, temperatureSouthPole = -15 } = config;
+ const tropics = [16, -20]; // tropics zone
+ const tropicalGradient = 0.15;
+
+ const tempNorthTropic = temperatureEquator - tropics[0] * tropicalGradient;
+ const northernGradient = (tempNorthTropic - temperatureNorthPole) / (90 - tropics[0]);
+
+ const tempSouthTropic = temperatureEquator + tropics[1] * tropicalGradient;
+ const southernGradient = (tempSouthTropic - temperatureSouthPole) / (90 + tropics[1]);
+
+ const exponent = config.heightExponent || 1.8;
+
+ for (let rowCellId = 0; rowCellId < cells.i.length; rowCellId += grid.cellsX) {
+ const [, y] = grid.points[rowCellId];
+ const rowLatitude = mapCoordinates.latN - (y / config.graphHeight) * mapCoordinates.latT; // [90; -90]
+ const tempSeaLevel = calculateSeaLevelTemp(rowLatitude);
+
+ for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) {
+ const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);
+ temp[cellId] = minmax(tempSeaLevel - tempAltitudeDrop, -128, 127);
+ }
+ }
+
+ function calculateSeaLevelTemp(latitude) {
+ const isTropical = latitude <= 16 && latitude >= -20;
+ if (isTropical) return temperatureEquator - Math.abs(latitude) * tropicalGradient;
+
+ return latitude > 0
+ ? tempNorthTropic - (latitude - tropics[0]) * northernGradient
+ : tempSouthTropic + (latitude - tropics[1]) * southernGradient;
+ }
+
+ // temperature drops by 6.5°C per 1km of altitude
+ function getAltitudeTemperatureDrop(h) {
+ if (h < 20) return 0;
+ const height = Math.pow(h - 18, exponent);
+ return rn((height / 1000) * 6.5);
+ }
+
+ TIME && console.timeEnd("calculateTemperatures");
+ return { temp };
+}
+
+// generate precipitation based on prevailing winds and elevation
+function generatePrecipitation(grid, mapCoordinates, config, utils) {
+ const { TIME, rn, minmax, rand } = utils;
+ TIME && console.time("generatePrecipitation");
+
+ const { cells, cellsX, cellsY } = grid;
+ const prec = new Uint8Array(cells.i.length); // precipitation array
+
+ const cellsDesired = config.cellsDesired || 10000;
+ const cellsNumberModifier = (cellsDesired / 10000) ** 0.25;
+ const precInputModifier = (config.precipitation || 100) / 100;
+ const modifier = cellsNumberModifier * precInputModifier;
+
+ const westerly = [];
+ const easterly = [];
+ let southerly = 0;
+ let northerly = 0;
+
+ // precipitation modifier per latitude band
+ const latitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
+ const MAX_PASSABLE_ELEVATION = 85;
+
+ // define wind directions based on cells latitude and prevailing winds there
+ for (let i = 0; i < cells.i.length; i += cellsX) {
+ const c = i;
+ const lat = mapCoordinates.latN - ((i / cellsX) / cellsY) * mapCoordinates.latT;
+ const latBand = Math.floor((Math.abs(lat) - 1) / 5);
+ const latMod = latitudeModifier[latBand] || 1;
+ const windTier = Math.floor(Math.abs(lat - 89) / 30); // 30d tiers from 0 to 5 from N to S
+ const { isWest, isEast, isNorth, isSouth } = getWindDirections(windTier, config.winds);
+
+ if (isWest) westerly.push([c, latMod, windTier]);
+ if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
+ if (isNorth) northerly++;
+ if (isSouth) southerly++;
+ }
+
+ // distribute winds by direction
+ if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX);
+ if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX);
+
+ const vertT = southerly + northerly;
+ if (northerly) {
+ const bandN = Math.floor((Math.abs(mapCoordinates.latN) - 1) / 5);
+ const latModN = mapCoordinates.latT > 60 ? latitudeModifier.reduce((a, b) => a + b) / latitudeModifier.length : latitudeModifier[bandN];
+ const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
+ const northRange = [];
+ for (let i = 0; i < cellsX; i++) northRange.push(i);
+ passWind(northRange, maxPrecN, cellsX, cellsY);
+ }
+
+ if (southerly) {
+ const bandS = Math.floor((Math.abs(mapCoordinates.latS) - 1) / 5);
+ const latModS = mapCoordinates.latT > 60 ? latitudeModifier.reduce((a, b) => a + b) / latitudeModifier.length : latitudeModifier[bandS];
+ const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
+ const southRange = [];
+ for (let i = cells.i.length - cellsX; i < cells.i.length; i++) southRange.push(i);
+ passWind(southRange, maxPrecS, -cellsX, cellsY);
+ }
+
+ function getWindDirections(tier, winds = []) {
+ const angle = winds[tier] || 225; // default southwest wind
+
+ const isWest = angle > 40 && angle < 140;
+ const isEast = angle > 220 && angle < 320;
+ const isNorth = angle > 100 && angle < 260;
+ const isSouth = angle > 280 || angle < 80;
+
+ return { isWest, isEast, isNorth, isSouth };
+ }
+
+ function passWind(source, maxPrec, next, steps) {
+ const maxPrecInit = maxPrec;
+
+ for (let first of source) {
+ if (first[0] !== undefined) {
+ maxPrec = Math.min(maxPrecInit * first[1], 255);
+ first = first[0];
+ }
+
+ let humidity = maxPrec - cells.h[first]; // initial water amount
+ if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry
+
+ for (let s = 0, current = first; s < steps; s++, current += next) {
+ if (cells.temp[current] < -5) continue; // no flux in permafrost
+
+ if (cells.h[current] < 20) {
+ // water cell
+ if (cells.h[current + next] >= 20) {
+ prec[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
+ } else {
+ humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell
+ prec[current] += 5 * modifier; // water cells precipitation
+ }
+ continue;
+ }
+
+ // land cell
+ const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION;
+ const precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity;
+ prec[current] += precipitation;
+ const evaporation = precipitation > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
+ humidity = isPassable ? minmax(humidity - precipitation + evaporation, 0, maxPrec) : 0;
+ }
+ }
+ }
+
+ function getPrecipitation(humidity, i, n) {
+ const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
+ const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height
+ const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
+ return minmax(normalLoss + diff * mod, 1, humidity);
+ }
+
+ TIME && console.timeEnd("generatePrecipitation");
+ return { prec };
+}
+
+export {
+ addLakesInDeepDepressions,
+ openNearSeaLakes,
+ defineMapSize,
+ calculateMapCoordinates,
+ calculateTemperatures,
+ generatePrecipitation
+};
\ No newline at end of file
diff --git a/procedural/src/engine/utils/graphUtils.js b/procedural/src/engine/utils/graphUtils.js
new file mode 100644
index 00000000..2a1f314e
--- /dev/null
+++ b/procedural/src/engine/utils/graphUtils.js
@@ -0,0 +1,405 @@
+"use strict";
+// FMG utils related to graph
+
+// check if new grid graph should be generated or we can use the existing one
+function shouldRegenerateGrid(grid, expectedSeed) {
+ if (expectedSeed && expectedSeed !== grid.seed) return true;
+
+ const cellsDesired = +byId("pointsInput").dataset.cells;
+ if (cellsDesired !== grid.cellsDesired) return true;
+
+ const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
+ const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
+ const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
+
+ return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
+}
+
+function generateGrid() {
+ Math.random = aleaPRNG(seed); // reset PRNG
+ const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
+ const {cells, vertices} = calculateVoronoi(points, boundary);
+ return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
+}
+
+// place random points to calculate Voronoi diagram
+function placePoints() {
+ TIME && console.time("placePoints");
+ const cellsDesired = +byId("pointsInput").dataset.cells;
+ const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
+
+ const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
+ const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
+ const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
+ const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
+ TIME && console.timeEnd("placePoints");
+
+ return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
+}
+
+// calculate Delaunay and then Voronoi diagram
+function calculateVoronoi(points, boundary) {
+ TIME && console.time("calculateDelaunay");
+ const allPoints = points.concat(boundary);
+ const delaunay = Delaunator.from(allPoints);
+ TIME && console.timeEnd("calculateDelaunay");
+
+ TIME && console.time("calculateVoronoi");
+ const voronoi = new Voronoi(delaunay, allPoints, points.length);
+
+ const cells = voronoi.cells;
+ cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
+ const vertices = voronoi.vertices;
+ TIME && console.timeEnd("calculateVoronoi");
+
+ return {cells, vertices};
+}
+
+// add points along map edge to pseudo-clip voronoi cells
+function getBoundaryPoints(width, height, spacing) {
+ const offset = rn(-1 * spacing);
+ const bSpacing = spacing * 2;
+ const w = width - offset * 2;
+ const h = height - offset * 2;
+ const numberX = Math.ceil(w / bSpacing) - 1;
+ const numberY = Math.ceil(h / bSpacing) - 1;
+ const points = [];
+
+ for (let i = 0.5; i < numberX; i++) {
+ let x = Math.ceil((w * i) / numberX + offset);
+ points.push([x, offset], [x, h + offset]);
+ }
+
+ for (let i = 0.5; i < numberY; i++) {
+ let y = Math.ceil((h * i) / numberY + offset);
+ points.push([offset, y], [w + offset, y]);
+ }
+
+ return points;
+}
+
+// get points on a regular square grid and jitter them a bit
+function getJitteredGrid(width, height, spacing) {
+ const radius = spacing / 2; // square radius
+ const jittering = radius * 0.9; // max deviation
+ const doubleJittering = jittering * 2;
+ const jitter = () => Math.random() * doubleJittering - jittering;
+
+ let points = [];
+ for (let y = radius; y < height; y += spacing) {
+ for (let x = radius; x < width; x += spacing) {
+ const xj = Math.min(rn(x + jitter(), 2), width);
+ const yj = Math.min(rn(y + jitter(), 2), height);
+ points.push([xj, yj]);
+ }
+ }
+ return points;
+}
+
+// return cell index on a regular square grid
+function findGridCell(x, y, grid) {
+ return (
+ Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
+ Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
+ );
+}
+
+// return array of cell indexes in radius on a regular square grid
+function findGridAll(x, y, radius) {
+ const c = grid.cells.c;
+ let r = Math.floor(radius / grid.spacing);
+ let found = [findGridCell(x, y, grid)];
+ if (!r || radius === 1) return found;
+ if (r > 0) found = found.concat(c[found[0]]);
+ if (r > 1) {
+ let frontier = c[found[0]];
+ while (r > 1) {
+ let cycle = frontier.slice();
+ frontier = [];
+ cycle.forEach(function (s) {
+ c[s].forEach(function (e) {
+ if (found.indexOf(e) !== -1) return;
+ found.push(e);
+ frontier.push(e);
+ });
+ });
+ r--;
+ }
+ }
+
+ return found;
+}
+
+// return closest pack points quadtree datum
+function find(x, y, radius = Infinity) {
+ return pack.cells.q.find(x, y, radius);
+}
+
+// return closest cell index
+function findCell(x, y, radius = Infinity) {
+ if (!pack.cells?.q) return;
+ const found = pack.cells.q.find(x, y, radius);
+ return found ? found[2] : undefined;
+}
+
+// return array of cell indexes in radius
+function findAll(x, y, radius) {
+ const found = pack.cells.q.findAll(x, y, radius);
+ return found.map(r => r[2]);
+}
+
+// get polygon points for packed cells knowing cell id
+function getPackPolygon(i) {
+ return pack.cells.v[i].map(v => pack.vertices.p[v]);
+}
+
+// get polygon points for initial cells knowing cell id
+function getGridPolygon(i) {
+ return grid.cells.v[i].map(v => grid.vertices.p[v]);
+}
+
+// mbostock's poissonDiscSampler
+function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
+ if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
+
+ const width = x1 - x0;
+ const height = y1 - y0;
+ const r2 = r * r;
+ const r2_3 = 3 * r2;
+ const cellSize = r * Math.SQRT1_2;
+ const gridWidth = Math.ceil(width / cellSize);
+ const gridHeight = Math.ceil(height / cellSize);
+ const grid = new Array(gridWidth * gridHeight);
+ const queue = [];
+
+ function far(x, y) {
+ const i = (x / cellSize) | 0;
+ const j = (y / cellSize) | 0;
+ const i0 = Math.max(i - 2, 0);
+ const j0 = Math.max(j - 2, 0);
+ const i1 = Math.min(i + 3, gridWidth);
+ const j1 = Math.min(j + 3, gridHeight);
+ for (let j = j0; j < j1; ++j) {
+ const o = j * gridWidth;
+ for (let i = i0; i < i1; ++i) {
+ const s = grid[o + i];
+ if (s) {
+ const dx = s[0] - x;
+ const dy = s[1] - y;
+ if (dx * dx + dy * dy < r2) return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ function sample(x, y) {
+ queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
+ return [x + x0, y + y0];
+ }
+
+ yield sample(width / 2, height / 2);
+
+ pick: while (queue.length) {
+ const i = (Math.random() * queue.length) | 0;
+ const parent = queue[i];
+
+ for (let j = 0; j < k; ++j) {
+ const a = 2 * Math.PI * Math.random();
+ const r = Math.sqrt(Math.random() * r2_3 + r2);
+ const x = parent[0] + r * Math.cos(a);
+ const y = parent[1] + r * Math.sin(a);
+ if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
+ yield sample(x, y);
+ continue pick;
+ }
+ }
+
+ const r = queue.pop();
+ if (i < queue.length) queue[i] = r;
+ }
+}
+
+// filter land cells
+function isLand(i) {
+ return pack.cells.h[i] >= 20;
+}
+
+// filter water cells
+function isWater(i) {
+ return pack.cells.h[i] < 20;
+}
+
+// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
+void (function addFindAll() {
+ const Quad = function (node, x0, y0, x1, y1) {
+ this.node = node;
+ this.x0 = x0;
+ this.y0 = y0;
+ this.x1 = x1;
+ this.y1 = y1;
+ };
+
+ const tree_filter = function (x, y, radius) {
+ const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
+ if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
+ radiusSearchInit(t, radius);
+
+ var i = 0;
+ while ((t.q = t.quads.pop())) {
+ i++;
+
+ // Stop searching if this quadrant can’t contain a closer node.
+ if (
+ !(t.node = t.q.node) ||
+ (t.x1 = t.q.x0) > t.x3 ||
+ (t.y1 = t.q.y0) > t.y3 ||
+ (t.x2 = t.q.x1) < t.x0 ||
+ (t.y2 = t.q.y1) < t.y0
+ )
+ continue;
+
+ // Bisect the current quadrant.
+ if (t.node.length) {
+ t.node.explored = true;
+ var xm = (t.x1 + t.x2) / 2,
+ ym = (t.y1 + t.y2) / 2;
+
+ t.quads.push(
+ new Quad(t.node[3], xm, ym, t.x2, t.y2),
+ new Quad(t.node[2], t.x1, ym, xm, t.y2),
+ new Quad(t.node[1], xm, t.y1, t.x2, ym),
+ new Quad(t.node[0], t.x1, t.y1, xm, ym)
+ );
+
+ // Visit the closest quadrant first.
+ if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
+ t.q = t.quads[t.quads.length - 1];
+ t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
+ t.quads[t.quads.length - 1 - t.i] = t.q;
+ }
+ }
+
+ // Visit this point. (Visiting coincident points isn’t necessary!)
+ else {
+ var dx = x - +this._x.call(null, t.node.data),
+ dy = y - +this._y.call(null, t.node.data),
+ d2 = dx * dx + dy * dy;
+ radiusSearchVisit(t, d2);
+ }
+ }
+ return t.result;
+ };
+ d3.quadtree.prototype.findAll = tree_filter;
+
+ var radiusSearchInit = function (t, radius) {
+ t.result = [];
+ (t.x0 = t.x - radius), (t.y0 = t.y - radius);
+ (t.x3 = t.x + radius), (t.y3 = t.y + radius);
+ t.radius = radius * radius;
+ };
+
+ var radiusSearchVisit = function (t, d2) {
+ t.node.data.scanned = true;
+ if (d2 < t.radius) {
+ do {
+ t.result.push(t.node.data);
+ t.node.data.selected = true;
+ } while ((t.node = t.node.next));
+ }
+ };
+})();
+
+// draw raster heightmap preview (not used in main generation)
+function drawHeights({heights, width, height, scheme, renderOcean}) {
+ const canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+ const imageData = ctx.createImageData(width, height);
+
+ const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
+
+ for (let i = 0; i < heights.length; i++) {
+ const color = scheme(1 - getHeight(heights[i]) / 100);
+ const {r, g, b} = d3.color(color);
+
+ const n = i * 4;
+ imageData.data[n] = r;
+ imageData.data[n + 1] = g;
+ imageData.data[n + 2] = b;
+ imageData.data[n + 3] = 255;
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+ return canvas.toDataURL("image/png");
+}
+
+// convert grid graph to pack cells by filtering and adding coastal points
+function reGraph(grid, utils) {
+ const { TIME, rn, createTypedArray, UINT16_MAX } = utils;
+ TIME && console.time("reGraph");
+
+ const { cells: gridCells, points, features } = grid;
+ const newCells = { p: [], g: [], h: [] }; // store new data
+ const spacing2 = grid.spacing ** 2;
+
+ for (const i of gridCells.i) {
+ const height = gridCells.h[i];
+ const type = gridCells.t[i];
+
+ if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points
+ if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points
+
+ const [x, y] = points[i];
+ addNewPoint(i, x, y, height);
+
+ // add additional points for cells along coast
+ if (type === 1 || type === -1) {
+ if (gridCells.b[i]) continue; // not for near-border cells
+ gridCells.c[i].forEach(function (e) {
+ if (i > e) return;
+ if (gridCells.t[e] === type) {
+ const dist2 = (y - points[e][1]) ** 2 + (x - points[e][0]) ** 2;
+ if (dist2 < spacing2) return; // too close to each other
+ const x1 = rn((x + points[e][0]) / 2, 1);
+ const y1 = rn((y + points[e][1]) / 2, 1);
+ addNewPoint(i, x1, y1, height);
+ }
+ });
+ }
+ }
+
+ function addNewPoint(i, x, y, height) {
+ newCells.p.push([x, y]);
+ newCells.g.push(i);
+ newCells.h.push(height);
+ }
+
+ const { cells: packCells, vertices } = calculateVoronoi(newCells.p, grid.boundary);
+ const pack = {
+ vertices,
+ cells: {
+ ...packCells,
+ p: newCells.p,
+ g: createTypedArray({ maxValue: grid.points.length, from: newCells.g }),
+ q: utils.d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])),
+ h: createTypedArray({ maxValue: 100, from: newCells.h }),
+ area: createTypedArray({ maxValue: UINT16_MAX, length: packCells.i.length }).map((_, cellId) => {
+ const polygon = pack.cells.v[cellId].map(v => pack.vertices.p[v]);
+ const area = Math.abs(utils.d3.polygonArea(polygon));
+ return Math.min(area, UINT16_MAX);
+ })
+ }
+ };
+
+ TIME && console.timeEnd("reGraph");
+ return pack;
+}
+
+
+export {
+ shouldRegenerateGrid, generateGrid, placePoints, calculateVoronoi, getBoundaryPoints,
+ getJitteredGrid, findGridCell, findGridAll, find, findCell, findAll,
+ getPackPolygon, getGridPolygon, poissonDiscSampler, isLand, isWater, drawHeights, reGraph
+};
diff --git a/procedural/src/engine/utils/index.js b/procedural/src/engine/utils/index.js
new file mode 100644
index 00000000..6d77fd96
--- /dev/null
+++ b/procedural/src/engine/utils/index.js
@@ -0,0 +1,27 @@
+import './polyfills.js';
+
+export { last, unique, deepCopy, getTypedArray, createTypedArray } from './arrayUtils.js';
+export { toHEX, getColors, getRandomColor, getMixedColor } from './colorUtils.js';
+export {
+ clipPoly, getSegmentId, debounce, throttle, parseError, getBase64, openURL,
+ wiki, link, isCtrlClick, generateDate, getLongitude, getLatitude, getCoordinates
+} from './commonUtils.js';
+export { drawCellsValue, drawPolygons, drawRouteConnections, drawPoint, drawPath } from './debugUtils.js';
+export { rollups, nest, dist2 } from './functionUtils.js';
+export {
+ shouldRegenerateGrid, generateGrid, placePoints, calculateVoronoi, getBoundaryPoints,
+ getJitteredGrid, findGridCell, findGridAll, find, findCell, findAll,
+ getPackPolygon, getGridPolygon, poissonDiscSampler, isLand, isWater, drawHeights, reGraph
+} from './graphUtils.js';
+export { removeParent, getComposedPath, getNextId, getAbsolutePath } from './nodeUtils.js';
+export { vowel, trimVowels, getAdjective, nth, abbreviate, list } from './languageUtils.js';
+export { rn, minmax, lim, normalize, lerp } from './numberUtils.js';
+export { getIsolines, getFillPath, getBorderPath, getVertexPath, getPolesOfInaccessibility, connectVertices, findPath, restorePath } from './pathUtils.js';
+export { rand, P, each, gauss, Pint, ra, rw, biased, getNumberInRange, generateSeed } from './probabilityUtils.js';
+export { round, capitalize, splitInTwo, parseTransform } from './stringUtils.js';
+export { convertTemperature, si, getInteger } from './unitUtils.js';
+export { aleaPRNG } from './alea.js';
+export { simplify } from './simplify.js';
+export { lineclip } from './lineclip.js';
+export { default as polylabel } from './polylabel.js';
+
diff --git a/procedural/src/engine/utils/languageUtils.js b/procedural/src/engine/utils/languageUtils.js
new file mode 100644
index 00000000..f79a31ab
--- /dev/null
+++ b/procedural/src/engine/utils/languageUtils.js
@@ -0,0 +1,176 @@
+"use strict";
+
+// chars that serve as vowels
+const VOWELS = `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`;
+function vowel(c) {
+ return VOWELS.includes(c);
+}
+
+// remove vowels from the end of the string
+function trimVowels(string, minLength = 3) {
+ while (string.length > minLength && vowel(last(string))) {
+ string = string.slice(0, -1);
+ }
+ return string;
+}
+
+const adjectivizationRules = [
+ {name: "guo", probability: 1, condition: new RegExp(" Guo$"), action: noun => noun.slice(0, -4)},
+ {
+ name: "orszag",
+ probability: 1,
+ condition: new RegExp("orszag$"),
+ action: noun => (noun.length < 9 ? noun + "ian" : noun.slice(0, -6))
+ },
+ {
+ name: "stan",
+ probability: 1,
+ condition: new RegExp("stan$"),
+ action: noun => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
+ },
+ {
+ name: "land",
+ probability: 1,
+ condition: new RegExp("land$"),
+ action: noun => {
+ if (noun.length > 9) return noun.slice(0, -4);
+ const root = trimVowels(noun.slice(0, -4), 0);
+ if (root.length < 3) return noun + "ic";
+ if (root.length < 4) return root + "lish";
+ return root + "ish";
+ }
+ },
+ {
+ name: "que",
+ probability: 1,
+ condition: new RegExp("que$"),
+ action: noun => noun.replace(/que$/, "can")
+ },
+ {
+ name: "a",
+ probability: 1,
+ condition: new RegExp("a$"),
+ action: noun => noun + "n"
+ },
+ {
+ name: "o",
+ probability: 1,
+ condition: new RegExp("o$"),
+ action: noun => noun.replace(/o$/, "an")
+ },
+ {
+ name: "u",
+ probability: 1,
+ condition: new RegExp("u$"),
+ action: noun => noun + "an"
+ },
+ {
+ name: "i",
+ probability: 1,
+ condition: new RegExp("i$"),
+ action: noun => noun + "an"
+ },
+ {
+ name: "e",
+ probability: 1,
+ condition: new RegExp("e$"),
+ action: noun => noun + "an"
+ },
+ {
+ name: "ay",
+ probability: 1,
+ condition: new RegExp("ay$"),
+ action: noun => noun + "an"
+ },
+ {
+ name: "os",
+ probability: 1,
+ condition: new RegExp("os$"),
+ action: noun => {
+ const root = trimVowels(noun.slice(0, -2), 0);
+ if (root.length < 4) return noun.slice(0, -1);
+ return root + "ian";
+ }
+ },
+ {
+ name: "es",
+ probability: 1,
+ condition: new RegExp("es$"),
+ action: noun => {
+ const root = trimVowels(noun.slice(0, -2), 0);
+ if (root.length > 7) return noun.slice(0, -1);
+ return root + "ian";
+ }
+ },
+ {
+ name: "l",
+ probability: 0.8,
+ condition: new RegExp("l$"),
+ action: noun => noun + "ese"
+ },
+ {
+ name: "n",
+ probability: 0.8,
+ condition: new RegExp("n$"),
+ action: noun => noun + "ese"
+ },
+ {
+ name: "ad",
+ probability: 0.8,
+ condition: new RegExp("ad$"),
+ action: noun => noun + "ian"
+ },
+ {
+ name: "an",
+ probability: 0.8,
+ condition: new RegExp("an$"),
+ action: noun => noun + "ian"
+ },
+ {
+ name: "ish",
+ probability: 0.25,
+ condition: new RegExp("^[a-zA-Z]{6}$"),
+ action: noun => trimVowels(noun.slice(0, -1)) + "ish"
+ },
+ {
+ name: "an",
+ probability: 0.5,
+ condition: new RegExp("^[a-zA-Z]{0,7}$"),
+ action: noun => trimVowels(noun) + "an"
+ }
+];
+
+// get adjective form from noun
+function getAdjective(noun) {
+ for (const rule of adjectivizationRules) {
+ if (P(rule.probability) && rule.condition.test(noun)) {
+ return rule.action(noun);
+ }
+ }
+ return noun; // no rule applied, return noun as is
+}
+
+// get ordinal from integer: 1 => 1st
+const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
+
+// get two-letters code (abbreviation) from string
+function abbreviate(name, restricted = []) {
+ const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
+ const words = parsed.split(" ");
+ const letters = words.join("");
+
+ let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
+ for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
+ code = letters[0] + letters[i].toUpperCase();
+ }
+ return code;
+}
+
+// conjunct array: [A,B,C] => "A, B and C"
+function list(array) {
+ if (!Intl.ListFormat) return array.join(", ");
+ const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
+ return conjunction.format(array);
+}
+
+export { vowel, trimVowels, getAdjective, nth, abbreviate, list };
diff --git a/procedural/src/engine/utils/lineclip.js b/procedural/src/engine/utils/lineclip.js
new file mode 100644
index 00000000..06329bc0
--- /dev/null
+++ b/procedural/src/engine/utils/lineclip.js
@@ -0,0 +1,3 @@
+// lineclip by mourner, https://github.com/mapbox/lineclip
+"use strict";function lineclip(t,e,n){var r,i,u,o,s,h=t.length,c=bitCode(t[0],e),f=[];for(n=n||[],r=1;re[2]&&(n|=2),t[1]e[3]&&(n|=8),n}
+export { lineclip };
\ No newline at end of file
diff --git a/procedural/src/engine/utils/nodeUtils.js b/procedural/src/engine/utils/nodeUtils.js
new file mode 100644
index 00000000..fb8be1ac
--- /dev/null
+++ b/procedural/src/engine/utils/nodeUtils.js
@@ -0,0 +1,32 @@
+"use strict";
+// FMG utils related to nodes
+
+// remove parent element (usually if child is clicked)
+function removeParent() {
+ this.parentNode.parentNode.removeChild(this.parentNode);
+}
+
+// polyfill for composedPath
+function getComposedPath(node) {
+ let parent;
+ if (node.parentNode) parent = node.parentNode;
+ else if (node.host) parent = node.host;
+ else if (node.defaultView) parent = node.defaultView;
+ if (parent !== undefined) return [node].concat(getComposedPath(parent));
+ return [node];
+}
+
+// get next unused id
+function getNextId(core, i = 1) {
+ while (document.getElementById(core + i)) i++;
+ return core + i;
+}
+
+function getAbsolutePath(href) {
+ if (!href) return "";
+ const link = document.createElement("a");
+ link.href = href;
+ return link.href;
+}
+
+export { removeParent, getComposedPath, getNextId, getAbsolutePath };
diff --git a/procedural/src/engine/utils/numberUtils.js b/procedural/src/engine/utils/numberUtils.js
new file mode 100644
index 00000000..9c374ccd
--- /dev/null
+++ b/procedural/src/engine/utils/numberUtils.js
@@ -0,0 +1,28 @@
+"use strict";
+// FMG utils related to numbers
+
+// round value to d decimals
+function rn(v, d = 0) {
+ const m = Math.pow(10, d);
+ return Math.round(v * m) / m;
+}
+
+function minmax(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+}
+
+// return value in range [0, 100]
+function lim(v) {
+ return minmax(v, 0, 100);
+}
+
+// normalization function
+function normalize(val, min, max) {
+ return minmax((val - min) / (max - min), 0, 1);
+}
+
+function lerp(a, b, t) {
+ return a + (b - a) * t;
+}
+
+export { rn, minmax, lim, normalize, lerp };
diff --git a/procedural/src/engine/utils/pathUtils.js b/procedural/src/engine/utils/pathUtils.js
new file mode 100644
index 00000000..5af145a5
--- /dev/null
+++ b/procedural/src/engine/utils/pathUtils.js
@@ -0,0 +1,237 @@
+"use strict";
+
+// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
+function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
+ const {cells, vertices} = graph;
+ const isolines = {};
+
+ const checkedCells = new Uint8Array(cells.i.length);
+ const addToChecked = cellId => (checkedCells[cellId] = 1);
+ const isChecked = cellId => checkedCells[cellId] === 1;
+
+ for (const cellId of cells.i) {
+ if (isChecked(cellId) || !getType(cellId)) continue;
+ addToChecked(cellId);
+
+ const type = getType(cellId);
+ const ofSameType = cellId => getType(cellId) === type;
+ const ofDifferentType = cellId => getType(cellId) !== type;
+
+ const onborderCell = cells.c[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ // check if inner lake. Note there is no shoreline for grid features
+ const feature = graph.features[cells.f[onborderCell]];
+ if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
+
+ const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
+ if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
+
+ const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
+ if (vertexChain.length < 3) continue;
+
+ addIsoline(type, vertices, vertexChain);
+ }
+
+ return isolines;
+
+ function addIsoline(type, vertices, vertexChain) {
+ if (!isolines[type]) isolines[type] = {};
+
+ if (options.polygons) {
+ if (!isolines[type].polygons) isolines[type].polygons = [];
+ isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
+ }
+
+ if (options.fill) {
+ if (!isolines[type].fill) isolines[type].fill = "";
+ isolines[type].fill += getFillPath(vertices, vertexChain);
+ }
+
+ if (options.waterGap) {
+ if (!isolines[type].waterGap) isolines[type].waterGap = "";
+ const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
+ isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
+ }
+
+ if (options.halo) {
+ if (!isolines[type].halo) isolines[type].halo = "";
+ const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
+ isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
+ }
+ }
+}
+
+function getFillPath(vertices, vertexChain) {
+ const points = vertexChain.map(vertexId => vertices.p[vertexId]);
+ const firstPoint = points.shift();
+ return `M${firstPoint} L${points.join(" ")} Z`;
+}
+
+function getBorderPath(vertices, vertexChain, discontinue) {
+ let discontinued = true;
+ let lastOperation = "";
+ const path = vertexChain.map(vertexId => {
+ if (discontinue(vertexId)) {
+ discontinued = true;
+ return "";
+ }
+
+ const operation = discontinued ? "M" : "L";
+ discontinued = false;
+ lastOperation = operation;
+
+ const command = operation === "L" && operation === lastOperation ? "" : operation;
+ return ` ${command}${vertices.p[vertexId]}`;
+ });
+
+ return path.join("").trim();
+}
+
+// get single path for an non-continuous array of cells
+function getVertexPath(cellsArray) {
+ const {cells, vertices} = pack;
+
+ const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
+ const ofSameType = cellId => cellsObj[cellId];
+ const ofDifferentType = cellId => !cellsObj[cellId];
+
+ const checkedCells = new Uint8Array(cells.c.length);
+ const addToChecked = cellId => (checkedCells[cellId] = 1);
+ const isChecked = cellId => checkedCells[cellId] === 1;
+
+ let path = "";
+
+ for (const cellId of cellsArray) {
+ if (isChecked(cellId)) continue;
+
+ const onborderCell = cells.c[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ const feature = pack.features[cells.f[onborderCell]];
+ if (feature.type === "lake" && feature.shoreline) {
+ if (feature.shoreline.every(ofSameType)) continue; // inner lake
+ }
+
+ const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
+ if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
+
+ const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
+ if (vertexChain.length < 3) continue;
+
+ path += getFillPath(vertices, vertexChain);
+ }
+
+ return path;
+}
+
+function getPolesOfInaccessibility(graph, getType) {
+ const isolines = getIsolines(graph, getType, {polygons: true});
+
+ const poles = Object.entries(isolines).map(([id, isoline]) => {
+ const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
+ const [x, y] = polylabel(multiPolygon, 20);
+ return [id, [rn(x), rn(y)]];
+ });
+
+ return Object.fromEntries(poles);
+}
+
+function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
+ const MAX_ITERATIONS = vertices.c.length;
+ const chain = []; // vertices chain to form a path
+
+ let next = startingVertex;
+ for (let i = 0; i === 0 || next !== startingVertex; i++) {
+ const previous = chain.at(-1);
+ const current = next;
+ chain.push(current);
+
+ const neibCells = vertices.c[current];
+ if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
+
+ const [c1, c2, c3] = neibCells.map(ofSameType);
+ const [v1, v2, v3] = vertices.v[current];
+
+ if (v1 !== previous && c1 !== c2) next = v1;
+ else if (v2 !== previous && c2 !== c3) next = v2;
+ else if (v3 !== previous && c1 !== c3) next = v3;
+
+ if (next >= vertices.c.length) {
+ ERROR && console.error("ConnectVertices: next vertex is out of bounds");
+ break;
+ }
+
+ if (next === current) {
+ ERROR && console.error("ConnectVertices: next vertex is not found");
+ break;
+ }
+
+ if (i === MAX_ITERATIONS) {
+ ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
+ break;
+ }
+ }
+
+ if (closeRing) chain.push(startingVertex);
+ return chain;
+}
+
+/**
+ * Finds the shortest path between two cells using a cost-based pathfinding algorithm.
+ * @param {number} start - The ID of the starting cell.
+ * @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
+ * @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
+ * @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
+ */
+function findPath(start, isExit, getCost) {
+ if (isExit(start)) return null;
+
+ const from = [];
+ const cost = [];
+ const queue = new FlatQueue();
+ queue.push(start, 0);
+
+ while (queue.length) {
+ const currentCost = queue.peekValue();
+ const current = queue.pop();
+
+ for (const next of pack.cells.c[current]) {
+ if (isExit(next)) {
+ from[next] = current;
+ return restorePath(next, start, from);
+ }
+
+ const nextCost = getCost(current, next);
+ if (nextCost === Infinity) continue; // impassable cell
+ const totalCost = currentCost + nextCost;
+
+ if (totalCost >= cost[next]) continue; // has cheaper path
+ from[next] = current;
+ cost[next] = totalCost;
+ queue.push(next, totalCost);
+ }
+ }
+
+ return null;
+}
+
+// supplementary function for findPath
+function restorePath(exit, start, from) {
+ const pathCells = [];
+
+ let current = exit;
+ let prev = exit;
+
+ while (current !== start) {
+ pathCells.push(current);
+ prev = from[current];
+ current = prev;
+ }
+
+ pathCells.push(current);
+
+ return pathCells.reverse();
+}
+
+export { getIsolines, getFillPath, getBorderPath, getVertexPath, getPolesOfInaccessibility, connectVertices, findPath, restorePath };
diff --git a/procedural/src/engine/utils/polyfills.js b/procedural/src/engine/utils/polyfills.js
new file mode 100644
index 00000000..ffc10f74
--- /dev/null
+++ b/procedural/src/engine/utils/polyfills.js
@@ -0,0 +1,41 @@
+"use strict";
+
+// replaceAll
+if (String.prototype.replaceAll === undefined) {
+ String.prototype.replaceAll = function (str, newStr) {
+ if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
+ return this.replace(new RegExp(str, "g"), newStr);
+ };
+}
+
+// flat
+if (Array.prototype.flat === undefined) {
+ Array.prototype.flat = function () {
+ return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
+ };
+}
+
+// at
+if (Array.prototype.at === undefined) {
+ Array.prototype.at = function (index) {
+ if (index < 0) index += this.length;
+ if (index < 0 || index >= this.length) return undefined;
+ return this[index];
+ };
+}
+
+// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
+if (ReadableStream.prototype[Symbol.asyncIterator] === undefined) {
+ ReadableStream.prototype[Symbol.asyncIterator] = async function* () {
+ const reader = this.getReader();
+ try {
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) return;
+ yield value;
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ };
+}
diff --git a/procedural/src/engine/utils/polylabel.js b/procedural/src/engine/utils/polylabel.js
new file mode 100644
index 00000000..73b95746
--- /dev/null
+++ b/procedural/src/engine/utils/polylabel.js
@@ -0,0 +1 @@
+!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).polylabel=t()}}(function(){return function t(n,e,r){function o(a,u){if(!e[a]){if(!n[a]){var f="function"==typeof require&&require;if(!u&&f)return f(a,!0);if(i)return i(a,!0);var h=new Error("Cannot find module '"+a+"'");throw h.code="MODULE_NOT_FOUND",h}var s=e[a]={exports:{}};n[a][0].call(s.exports,function(t){var e=n[a][1][t];return o(e||t)},s,s.exports,t,n,e,r)}return e[a].exports}for(var i="function"==typeof require&&require,a=0;an!=l[1]>n&&t<(l[0]-d[0])*(n-d[1])/(l[1]-d[1])+d[0]&&(r=!r),o=Math.min(o,a(t,n,d,l))}return(r?1:-1)*Math.sqrt(o)}(t,n,r),this.max=this.d+this.h*Math.SQRT2}function a(t,n,e,r){var o=e[0],i=e[1],a=r[0]-o,u=r[1]-i;if(0!==a||0!==u){var f=((t-o)*a+(n-i)*u)/(a*a+u*u);f>1?(o=r[0],i=r[1]):f>0&&(o+=a*f,i+=u*f)}return(a=t-o)*a+(u=n-i)*u}n.exports=function(t,n,e){var a,u,f,h;n=n||1;for(var s=0;sf)&&(f=d[0]),(!s||d[1]>h)&&(h=d[1])}for(var l=f-a,p=h-u,c=Math.min(l,p),v=c/2,g=new r(null,o),x=a;xw.d&&(w=b,e&&console.info("found best %d after %d probes",Math.round(1e4*b.d)/1e4,m)),b.max-w.d<=n||(v=b.h/2,g.push(new i(b.x-v,b.y-v,v,t)),g.push(new i(b.x+v,b.y-v,v,t)),g.push(new i(b.x-v,b.y+v,v,t)),g.push(new i(b.x+v,b.y+v,v,t)),m+=4)}e&&(console.info("num probes: "+m),console.info("best distance: "+w.d));return[w.x,w.y]}},{tinyqueue:2}],2:[function(t,n,e){"use strict";function r(t,n){if(!(this instanceof r))return new r(t,n);if(this.data=t||[],this.length=this.data.length,this.compare=n||o,t)for(var e=Math.floor(this.length/2);e>=0;e--)this._down(e)}function o(t,n){return tn?1:0}function i(t,n,e){var r=t[n];t[n]=t[e],t[e]=r}n.exports=r,r.prototype={push:function(t){this.data.push(t),this.length++,this._up(this.length-1)},pop:function(){var t=this.data[0];return this.data[0]=this.data[this.length-1],this.length--,this.data.pop(),this._down(0),t},peek:function(){return this.data[0]},_up:function(t){for(var n=this.data,e=this.compare;t>0;){var r=Math.floor((t-1)/2);if(!(e(n[t],n[r])<0))break;i(n,r,t),t=r}},_down:function(t){for(var n=this.data,e=this.compare,r=this.length;;){var o=2*t+1,a=o+1,u=t;if(o= 1) return true;
+ if (probability <= 0) return false;
+ return Math.random() < probability;
+}
+
+function each(n) {
+ return i => i % n === 0;
+}
+
+/* Random Gaussian number generator
+ * @param {number} expected - expected value
+ * @param {number} deviation - standard deviation
+ * @param {number} min - minimum value
+ * @param {number} max - maximum value
+ * @param {number} round - round value to n decimals
+ * @return {number} random number
+ */
+function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
+ return rn(minmax(d3.randomNormal(expected, deviation)(), min, max), round);
+}
+
+// probability shorthand for floats
+function Pint(float) {
+ return ~~float + +P(float % 1);
+}
+
+// return random value from the array
+function ra(array) {
+ return array[Math.floor(Math.random() * array.length)];
+}
+
+// return random value from weighted array {"key1":weight1, "key2":weight2}
+function rw(object) {
+ const array = [];
+ for (const key in object) {
+ for (let i = 0; i < object[key]; i++) {
+ array.push(key);
+ }
+ }
+ return array[Math.floor(Math.random() * array.length)];
+}
+
+// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
+function biased(min, max, ex) {
+ return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
+}
+
+// get number from string in format "1-3" or "2" or "0.5"
+function getNumberInRange(r) {
+ if (typeof r !== "string") {
+ ERROR && console.error("Range value should be a string", r);
+ return 0;
+ }
+ if (!isNaN(+r)) return ~~r + +P(r - ~~r);
+ const sign = r[0] === "-" ? -1 : 1;
+ if (isNaN(+r[0])) r = r.slice(1);
+ const range = r.includes("-") ? r.split("-") : null;
+ if (!range) {
+ ERROR && console.error("Cannot parse the number. Check the format", r);
+ return 0;
+ }
+ const count = rand(range[0] * sign, +range[1]);
+ if (isNaN(count) || count < 0) {
+ ERROR && console.error("Cannot parse number. Check the format", r);
+ return 0;
+ }
+ return count;
+}
+
+function generateSeed() {
+ return String(Math.floor(Math.random() * 1e9));
+}
+
+export { rand, P, each, gauss, Pint, ra, rw, biased, getNumberInRange, generateSeed };
diff --git a/procedural/src/engine/utils/simplify.js b/procedural/src/engine/utils/simplify.js
new file mode 100644
index 00000000..c227efc1
--- /dev/null
+++ b/procedural/src/engine/utils/simplify.js
@@ -0,0 +1,100 @@
+/*
+ (c) 2017, Vladimir Agafonkin
+ Simplify.js, a high-performance JS polyline simplification library
+ mourner.github.io/simplify-js
+*/
+
+ // square distance between 2 points
+ function getSqDist([x1, y1], [x2, y2]) {
+ const dx = x1 - x2;
+ const dy = y1 - y2;
+
+ return dx * dx + dy * dy;
+ }
+
+ // square distance from a point to a segment
+ function getSqSegDist([x1, y1], [x, y], [x2, y2]) {
+ let dx = x2 - x;
+ let dy = y2 - y;
+
+ if (dx !== 0 || dy !== 0) {
+ const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy);
+
+ if (t > 1) {
+ x = x2;
+ y = y2;
+ } else if (t > 0) {
+ x += dx * t;
+ y += dy * t;
+ }
+ }
+
+ dx = x1 - x;
+ dy = y1 - y;
+
+ return dx * dx + dy * dy;
+ }
+ // rest of the code doesn't care about point format
+
+ // basic distance-based simplification
+ function simplifyRadialDist(points, sqTolerance) {
+ let prevPoint = points[0];
+ const newPoints = [prevPoint];
+ let point;
+
+ for (let i = 1; i < points.length; i++) {
+ point = points[i];
+ if (!point) continue;
+
+ if (getSqDist(point, prevPoint) > sqTolerance) {
+ newPoints.push(point);
+ prevPoint = point;
+ }
+ }
+
+ if (prevPoint !== point) newPoints.push(point);
+ return newPoints;
+ }
+
+ function simplifyDPStep(points, first, last, sqTolerance, simplified) {
+ let maxSqDist = sqTolerance;
+ let index = first;
+
+ for (let i = first + 1; i < last; i++) {
+ const sqDist = getSqSegDist(points[i], points[first], points[last]);
+
+ if (sqDist > maxSqDist) {
+ index = i;
+ maxSqDist = sqDist;
+ }
+ }
+
+ if (maxSqDist > sqTolerance) {
+ if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
+ simplified.push(points[index]);
+ if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
+ }
+ }
+
+ // simplification using Ramer-Douglas-Peucker algorithm
+ function simplifyDouglasPeucker(points, sqTolerance) {
+ const last = points.length - 1;
+
+ const simplified = [points[0]];
+ simplifyDPStep(points, 0, last, sqTolerance, simplified);
+ simplified.push(points[last]);
+
+ return simplified;
+ }
+
+ // both algorithms combined for awesome performance
+ export function simplify(points, tolerance, highestQuality = false) {
+ if (points.length <= 2) return points;
+
+ const sqTolerance = tolerance * tolerance;
+
+ points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
+ points = simplifyDouglasPeucker(points, sqTolerance);
+
+ return points;
+ }
diff --git a/procedural/src/engine/utils/stringUtils.js b/procedural/src/engine/utils/stringUtils.js
new file mode 100644
index 00000000..0f1f59fc
--- /dev/null
+++ b/procedural/src/engine/utils/stringUtils.js
@@ -0,0 +1,68 @@
+"use strict";
+// FMG utils related to strings
+
+// round numbers in string to d decimals
+function round(s, d = 1) {
+ return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
+ return rn(n, d);
+ });
+}
+
+// return string with 1st char capitalized
+function capitalize(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+// split string into 2 almost equal parts not breaking words
+function splitInTwo(str) {
+ const half = str.length / 2;
+ const ar = str.split(" ");
+ if (ar.length < 2) return ar; // only one word
+ let first = "",
+ last = "",
+ middle = "",
+ rest = "";
+
+ ar.forEach((w, d) => {
+ if (d + 1 !== ar.length) w += " ";
+ rest += w;
+ if (!first || rest.length < half) first += w;
+ else if (!middle) middle = w;
+ else last += w;
+ });
+
+ if (!last) return [first, middle];
+ if (first.length < last.length) return [first + middle, last];
+ return [first, middle + last];
+}
+
+// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
+function parseTransform(string) {
+ if (!string) return [0, 0, 0, 0, 0, 1];
+
+ const a = string
+ .replace(/[a-z()]/g, "")
+ .replace(/[ ]/g, ",")
+ .split(",");
+ return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
+}
+
+// check if string is a valid for JSON parse
+JSON.isValid = str => {
+ try {
+ JSON.parse(str);
+ } catch (e) {
+ return false;
+ }
+ return true;
+};
+
+JSON.safeParse = str => {
+ try {
+ return JSON.parse(str);
+ } catch (e) {
+ return null;
+ }
+};
+
+export { round, capitalize, splitInTwo, parseTransform };
diff --git a/procedural/src/engine/utils/unitUtils.js b/procedural/src/engine/utils/unitUtils.js
new file mode 100644
index 00000000..5698fa12
--- /dev/null
+++ b/procedural/src/engine/utils/unitUtils.js
@@ -0,0 +1,39 @@
+"use strict";
+// FMG utils related to units
+
+// conver temperature from °C to other scales
+const temperatureConversionMap = {
+ "°C": temp => rn(temp) + "°C",
+ "°F": temp => rn((temp * 9) / 5 + 32) + "°F",
+ K: temp => rn(temp + 273.15) + "K",
+ "°R": temp => rn(((temp + 273.15) * 9) / 5) + "°R",
+ "°De": temp => rn(((100 - temp) * 3) / 2) + "°De",
+ "°N": temp => rn((temp * 33) / 100) + "°N",
+ "°Ré": temp => rn((temp * 4) / 5) + "°Ré",
+ "°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø"
+};
+
+function convertTemperature(temp, scale = temperatureScale.value || "°C") {
+ return temperatureConversionMap[scale](temp);
+}
+
+// corvent number to short string with SI postfix
+function si(n) {
+ if (n >= 1e9) return rn(n / 1e9, 1) + "B";
+ if (n >= 1e8) return rn(n / 1e6) + "M";
+ if (n >= 1e6) return rn(n / 1e6, 1) + "M";
+ if (n >= 1e4) return rn(n / 1e3) + "K";
+ if (n >= 1e3) return rn(n / 1e3, 1) + "K";
+ return rn(n);
+}
+
+// getInteger number from user input data
+function getInteger(value) {
+ const metric = value.slice(-1);
+ if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
+ if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
+ if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
+ return parseInt(value);
+}
+
+export { convertTemperature, si, getInteger };
diff --git a/procedural/src/libs/d3.min.js b/procedural/src/libs/d3.min.js
new file mode 100644
index 00000000..8ed42731
--- /dev/null
+++ b/procedural/src/libs/d3.min.js
@@ -0,0 +1,2 @@
+// https://d3js.org v5.8.0 Copyright 2019 Mike Bostock
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}var r=e(n),i=r.right,o=r.left;function a(t,n){return[t,n]}function u(t){return null===t?NaN:+t}function c(t,n){var e,r,i=t.length,o=0,a=-1,c=0,f=0;if(null==n)for(;++a1)return f/(o-1)}function f(t,n){var e=c(t,n);return e?Math.sqrt(e):e}function s(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=y?10:o>=_?5:o>=b?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=y?10:o>=_?5:o>=b?2:1)}function w(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=y?i*=10:o>=_?i*=5:o>=b&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),a=+e(t[o],o,t);return a+(+e(t[o+1],o+1,t)-a)*(i-o)}}function A(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function k(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function S(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function T(t){if(!(i=t.length))return[];for(var n=-1,e=S(t,E),r=new Array(e);++n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}})),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),V.hasOwnProperty(n)?{space:V[n],local:t}:t}function W(t){var n=$(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===G&&n.documentElement.namespaceURI===G?n.createElement(t):n.createElementNS(e,t)}})(n)}function Z(){}function Q(t){return null==t?Z:function(){return this.querySelector(t)}}function J(){return[]}function K(t){return null==t?J:function(){return this.querySelectorAll(t)}}function tt(t){return function(){return this.matches(t)}}function nt(t){return new Array(t.length)}function et(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}et.prototype={constructor:et,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var rt="$";function it(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function ut(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function ct(t,n){return t.style.getPropertyValue(n)||ut(t).getComputedStyle(t,null).getPropertyValue(n)}function ft(t){return t.trim().split(/^|\s+/)}function st(t){return t.classList||new lt(t)}function lt(t){this._node=t,this._names=ft(t.getAttribute("class")||"")}function ht(t,n){for(var e=st(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var wt={};(t.event=null,"undefined"!=typeof document)&&("onmouseenter"in document.documentElement||(wt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Mt(t,n,e){return t=Nt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function Nt(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=x&&(x=m+1);!(b=y[x])&&++x=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=at);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):ct(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=ft(t+"");if(arguments.length<2){for(var r=st(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?kt:At,null==e&&(e=!1),r=0;r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):(n=rn.exec(t))?dn(parseInt(n[1],16)):(n=on.exec(t))?new yn(n[1],n[2],n[3],1):(n=an.exec(t))?new yn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?pn(n[1],n[2],n[3],n[4]):(n=cn.exec(t))?pn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=fn.exec(t))?bn(n[1],n[2]/100,n[3]/100,1):(n=sn.exec(t))?bn(n[1],n[2]/100,n[3]/100,n[4]):ln.hasOwnProperty(t)?dn(ln[t]):"transparent"===t?new yn(NaN,NaN,NaN,0):null}function dn(t){return new yn(t>>16&255,t>>8&255,255&t,1)}function pn(t,n,e,r){return r<=0&&(t=n=e=NaN),new yn(t,n,e,r)}function vn(t){return t instanceof Jt||(t=hn(t)),t?new yn((t=t.rgb()).r,t.g,t.b,t.opacity):new yn}function gn(t,n,e,r){return 1===arguments.length?vn(t):new yn(t,n,e,null==r?1:r)}function yn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function _n(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function bn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new xn(t,n,e,r)}function mn(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof xn)return new xn(t.h,t.s,t.l,t.opacity);if(t instanceof Jt||(t=hn(t)),!t)return new xn;if(t instanceof xn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new xn(a,u,c,t.opacity)}(t):new xn(t,n,e,null==r?1:r)}function xn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function wn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}Zt(Jt,hn,{displayable:function(){return this.rgb().displayable()},hex:function(){return this.rgb().hex()},toString:function(){return this.rgb()+""}}),Zt(yn,gn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return 0<=this.r&&this.r<=255&&0<=this.g&&this.g<=255&&0<=this.b&&this.b<=255&&0<=this.opacity&&this.opacity<=1},hex:function(){return"#"+_n(this.r)+_n(this.g)+_n(this.b)},toString:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),Zt(xn,mn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new yn(wn(t>=240?t-240:t+120,i,r),wn(t,i,r),wn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Mn=Math.PI/180,Nn=180/Math.PI,An=.96422,kn=1,Sn=.82521,Tn=4/29,En=6/29,Cn=3*En*En,Pn=En*En*En;function zn(t){if(t instanceof qn)return new qn(t.l,t.a,t.b,t.opacity);if(t instanceof Fn){if(isNaN(t.h))return new qn(t.l,0,0,t.opacity);var n=t.h*Mn;return new qn(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof yn||(t=vn(t));var e,r,i=On(t.r),o=On(t.g),a=On(t.b),u=Dn((.2225045*i+.7168786*o+.0606169*a)/kn);return i===o&&o===a?e=r=u:(e=Dn((.4360747*i+.3850649*o+.1430804*a)/An),r=Dn((.0139322*i+.0971045*o+.7141733*a)/Sn)),new qn(116*u-16,500*(e-u),200*(u-r),t.opacity)}function Rn(t,n,e,r){return 1===arguments.length?zn(t):new qn(t,n,e,null==r?1:r)}function qn(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Dn(t){return t>Pn?Math.pow(t,1/3):t/Cn+Tn}function Ln(t){return t>En?t*t*t:Cn*(t-Tn)}function Un(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function On(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Yn(t){if(t instanceof Fn)return new Fn(t.h,t.c,t.l,t.opacity);if(t instanceof qn||(t=zn(t)),0===t.a&&0===t.b)return new Fn(NaN,0,t.l,t.opacity);var n=Math.atan2(t.b,t.a)*Nn;return new Fn(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Bn(t,n,e,r){return 1===arguments.length?Yn(t):new Fn(t,n,e,null==r?1:r)}function Fn(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}Zt(qn,Rn,Qt(Jt,{brighter:function(t){return new qn(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new qn(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return new yn(Un(3.1338561*(n=An*Ln(n))-1.6168667*(t=kn*Ln(t))-.4906146*(e=Sn*Ln(e))),Un(-.9787684*n+1.9161415*t+.033454*e),Un(.0719453*n-.2289914*t+1.4052427*e),this.opacity)}})),Zt(Fn,Bn,Qt(Jt,{brighter:function(t){return new Fn(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Fn(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return zn(this).rgb()}}));var In=-.14861,Hn=1.78277,jn=-.29227,Xn=-.90649,Gn=1.97294,Vn=Gn*Xn,$n=Gn*Hn,Wn=Hn*jn-Xn*In;function Zn(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof Qn)return new Qn(t.h,t.s,t.l,t.opacity);t instanceof yn||(t=vn(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(Wn*r+Vn*n-$n*e)/(Wn+Vn-$n),o=r-i,a=(Gn*(e-i)-jn*o)/Xn,u=Math.sqrt(a*a+o*o)/(Gn*i*(1-i)),c=u?Math.atan2(a,o)*Nn-120:NaN;return new Qn(c<0?c+360:c,u,i,t.opacity)}(t):new Qn(t,n,e,null==r?1:r)}function Qn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Jn(t,n,e,r,i){var o=t*t,a=o*t;return((1-3*t+3*o-a)*n+(4-6*o+3*a)*e+(1+3*t+3*o-3*a)*r+a*i)/6}function Kn(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):ne(isNaN(t)?n:t)}function ie(t){return 1==(t=+t)?oe:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):ne(isNaN(n)?e:n)}}function oe(t,n){var e=n-t;return e?ee(t,e):ne(isNaN(t)?n:t)}Zt(Qn,Zn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Qn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Qn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Mn,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new yn(255*(n+e*(In*r+Hn*i)),255*(n+e*(jn*r+Xn*i)),255*(n+e*(Gn*r)),this.opacity)}}));var ae=function t(n){var e=ie(n);function r(t,n){var r=e((t=gn(t)).r,(n=gn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=oe(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function ue(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:he(e,r)})),o=ve.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:he(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:he(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:he(t,e)},{i:u-2,x:he(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(null,t),n=n._next;--Ge}function ar(){Qe=(Ze=Ke.now())+Je,Ge=Ve=0;try{or()}finally{Ge=0,function(){var t,n,e=je,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:je=n);Xe=t,cr(r)}(),Qe=0}}function ur(){var t=Ke.now(),n=t-Ze;n>We&&(Je-=n,Ze=t)}function cr(t){Ge||(Ve&&(Ve=clearTimeout(Ve)),t-Qe>24?(t<1/0&&(Ve=setTimeout(ar,t-Ke.now()-Je)),$e&&($e=clearInterval($e))):($e||(Ze=Ke.now(),$e=setInterval(ur,We)),Ge=1,tr(ar)))}function fr(t,n,e){var r=new rr;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}rr.prototype=ir.prototype={constructor:rr,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?nr():+e)+(null==n?0:+n),this._next||Xe===this||(Xe?Xe._next=this:je=this,Xe=this),this._call=t,this._time=e,cr()},stop:function(){this._call&&(this._call=null,this._time=1/0,cr())}};var sr=I("start","end","cancel","interrupt"),lr=[],hr=0,dr=1,pr=2,vr=3,gr=4,yr=5,_r=6;function br(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(c){var f,s,l,h;if(e.state!==dr)return u();for(f in i)if((h=i[f]).name===e.name){if(h.state===vr)return fr(o);h.state===gr?(h.state=_r,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fhr)throw new Error("too late; already scheduled");return e}function xr(t,n){var e=wr(t,n);if(e.state>vr)throw new Error("too late; already running");return e}function wr(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Mr(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>pr&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?mr:xr;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=$(t),r="transform"===e?Te:Ar;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttributeNS(t.space,t.local))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttribute(t))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttribute(t)}})(e,r,Nr(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttributeNS(t.space,t.local);return a===o?null:a===r?i:i=n(r=a,e)}}:function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttribute(t);return a===o?null:a===r?i:i=n(r=a,e)}})(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=$(t);return this.tween(e,(r.local?function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttributeNS(t.space,t.local,n(e))}}(t,i)),e}return i._value=n,i}:function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttribute(t,n(e))}}(t,i)),e}return i._value=n,i})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?Se:Ar;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=ct(this,t),a=(this.style.removeProperty(t),ct(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,Sr(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=ct(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=ct(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Nr(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=xr(this,t),f=c.on,s=null==c.value[a]?o||(o=Sr(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=ct(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n(r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Nr(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},remove:function(){return this.on("end.remove",(t=this._id,function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}));var t},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=wr(this.node(),e).tween,o=0,a=i.length;o0&&(r=o-p),M<0?h=d-v:M>0&&(a=u-v),x=vi,L.attr("cursor",xi.selection),Y());break;default:return}di()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:P&&(y=_=P=!1,Y());break;case 18:x===yi&&(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=gi,Y());break;case 32:x===vi&&(t.event.altKey?(w&&(s=l-p*w,r=o+p*w),M&&(h=d-v*M,a=u+v*M),x=yi):(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=gi),L.attr("cursor",xi[m]),Y());break;default:return}di()},!0).on("mousemove.brush",O,!0).on("mouseup.brush",B,!0);It(t.event.view)}hi(),Mr(b),c.call(b),q.start()}function O(){var t=Ot(b);!P||y||_||(Math.abs(t[0]-R[0])>Math.abs(t[1]-R[1])?_=!0:y=!0),R=t,g=!0,di(),Y()}function Y(){var t;switch(p=R[0]-z[0],v=R[1]-z[1],x){case vi:case pi:w&&(p=Math.max(S-r,Math.min(E-s,p)),o=r+p,l=s+p),M&&(v=Math.max(T-a,Math.min(C-h,v)),u=a+v,d=h+v);break;case gi:w<0?(p=Math.max(S-r,Math.min(E-r,p)),o=r+p,l=s):w>0&&(p=Math.max(S-s,Math.min(E-s,p)),o=r,l=s+p),M<0?(v=Math.max(T-a,Math.min(C-a,v)),u=a+v,d=h):M>0&&(v=Math.max(T-h,Math.min(C-h,v)),u=a,d=h+v);break;case yi:w&&(o=Math.max(S,Math.min(E,r-p*w)),l=Math.max(S,Math.min(E,s+p*w))),M&&(u=Math.max(T,Math.min(C,a-v*M)),d=Math.max(T,Math.min(C,h+v*M)))}l1e-6)if(Math.abs(s*u-c*f)>1e-6&&i){var h=e-o,d=r-a,p=u*u+c*c,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Bi-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*f)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>f*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+f:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-f)>1e-6)&&(this._+="L"+c+","+f),e&&(l<0&&(l=l%Fi+Fi),l>Ii?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=c)+","+(this._y1=f):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Bi)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};function Zi(){}function Qi(t,n){var e=new Zi;if(t instanceof Zi)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function so(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function lo(){}var ho=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function po(){var t=1,n=1,e=M,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(ao);else{var r=s(t),i=r[0],a=r[1];n=w(i,a,n),n=g(Math.floor(i/n)*n,Math.floor(a/n)*n,n)}return n.map(function(n){return o(t,n)})}function o(e,i){var o=[],u=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=e[0]>=r,ho[f<<1].forEach(p);for(;++o=r,ho[c|f<<1].forEach(p);ho[f<<0].forEach(p);for(;++u=r,s=e[u*t]>=r,ho[f<<1|s<<2].forEach(p);++o=r,l=s,s=e[u*t+o+1]>=r,ho[c|f<<1|s<<2|l<<3].forEach(p);ho[f|s<<3].forEach(p)}o=-1,s=e[u*t]>=r,ho[s<<2].forEach(p);for(;++o=r,ho[s<<2|l<<3].forEach(p);function p(t){var n,e,r=[t[0][0]+o,t[0][1]+u],c=[t[1][0]+o,t[1][1]+u],f=a(r),s=a(c);(n=d[f])?(e=h[s])?(delete d[n.end],delete h[e.start],n===e?(n.ring.push(c),i(n.ring)):h[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete d[n.end],n.ring.push(c),d[n.end=s]=n):(n=h[s])?(e=d[f])?(delete h[n.start],delete d[e.end],n===e?(n.ring.push(c),i(n.ring)):h[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete h[n.start],n.ring.unshift(r),h[n.start=f]=n):h[f]=d[s]={start:f,end:s,ring:[r,c]}}ho[s<<3].forEach(p)}(e,i,function(t){r(t,e,i),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?o.push([t]):u.push(t)}),u.forEach(function(t){for(var n,e=0,r=o.length;e0&&a0&&u0&&o>0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?uo(oo.call(t)):uo(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:lo,i):r===u},i}function vo(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[u-o+a*r]),n.data[u-e+a*r]=c/Math.min(u+1,r-1+o-u,o))}function go(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=c/Math.min(u+1,i-1+o-u,o))}function yo(t){return t[0]}function _o(t){return t[1]}function bo(){return 1}var mo={},xo={},wo=34,Mo=10,No=13;function Ao(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function ko(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return xo;if(f)return f=!1,mo;var n,r,i=a;if(t.charCodeAt(i)===wo){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Mo?f=!0:r===No&&(f=!0,t.charCodeAt(a)===Mo&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;a=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Jo(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Ko(t){return t[0]}function ta(t){return t[1]}function na(t,n,e){var r=new ea(null==n?Ko:n,null==e?ta:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function ea(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function ra(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var ia=na.prototype=ea.prototype;function oa(t){return t.x+t.vx}function aa(t){return t.y+t.vy}function ua(t){return t.index}function ca(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function fa(t){return t.x}function sa(t){return t.y}ia.copy=function(){var t,n,e=new ea(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ra(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ra(n));return e},ia.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return Qo(this.cover(n,e),n,e,t)},ia.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));for(st||t>i||r>n||n>o))return this;var a,u,c=i-e,f=this._root;switch(u=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{(a=new Array(4))[u]=f,f=a}while(o=r+(c*=2),t>(i=e+c)||n>o);break;case 1:do{(a=new Array(4))[u]=f,f=a}while(o=r+(c*=2),(e=i-c)>t||n>o);break;case 2:do{(a=new Array(4))[u]=f,f=a}while(r=o-(c*=2),t>(i=e+c)||r>n);break;case 3:do{(a=new Array(4))[u]=f,f=a}while(r=o-(c*=2),(e=i-c)>t||r>n)}this._root&&this._root.length&&(this._root=f)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},ia.data=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},ia.extent=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},ia.find=function(t,n,e){var r,i,o,a,u,c,f,s=this._x0,l=this._y0,h=this._x1,d=this._y1,p=[],v=this._root;for(v&&p.push(new Jo(v,s,l,h,d)),null==e?e=1/0:(s=t-e,l=n-e,h=t+e,d=n+e,e*=e);c=p.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>d||(a=c.x1)=y)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(c=(v+y)/2))?v=c:y=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},ia.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function pa(t){return(t=da(Math.abs(t)))?t[1]:NaN}var va,ga=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function ya(t){return new _a(t)}function _a(t){if(!(n=ga.exec(t)))throw new Error("invalid format: "+t);var n;this.fill=n[1]||" ",this.align=n[2]||">",this.sign=n[3]||"-",this.symbol=n[4]||"",this.zero=!!n[5],this.width=n[6]&&+n[6],this.comma=!!n[7],this.precision=n[8]&&+n[8].slice(1),this.trim=!!n[9],this.type=n[10]||""}function ba(t,n){var e=da(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}ya.prototype=_a.prototype,_a.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var ma={"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return ba(100*t,n)},r:ba,s:function(t,n){var e=da(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(va=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+da(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function xa(t){return t}var wa,Ma=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Na(t){var n,e,r=t.grouping&&t.thousands?(n=t.grouping,e=t.thousands,function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}):xa,i=t.currency,o=t.decimal,a=t.numerals?function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(t.numerals):xa,u=t.percent||"%";function c(t){var n=(t=ya(t)).fill,e=t.align,c=t.sign,f=t.symbol,s=t.zero,l=t.width,h=t.comma,d=t.precision,p=t.trim,v=t.type;"n"===v?(h=!0,v="g"):ma[v]||(null==d&&(d=12),p=!0,v="g"),(s||"0"===n&&"="===e)&&(s=!0,n="0",e="=");var g="$"===f?i[0]:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",y="$"===f?i[1]:/[%p]/.test(v)?u:"",_=ma[v],b=/[defgprs%]/.test(v);function m(t){var i,u,f,m=g,x=y;if("c"===v)x=_(t)+x,t="";else{var w=(t=+t)<0;if(t=_(Math.abs(t),d),p&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0){if(!+t[r])break t;i=0}}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),w&&0==+t&&(w=!1),m=(w?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=("s"===v?Ma[8+va/3]:"")+x+(w&&"("===c?")":""),b)for(i=-1,u=t.length;++i(f=t.charCodeAt(i))||f>57){x=(46===f?o+t.slice(i+1):t.slice(i))+x,t=t.slice(0,i);break}}h&&!s&&(t=r(t,1/0));var M=m.length+t.length+x.length,N=M>1)+m+t+x+N.slice(M);break;default:t=N+m+t+x}return a(t)}return d=null==d?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,d)):Math.max(0,Math.min(20,d)),m.toString=function(){return t+""},m}return{format:c,formatPrefix:function(t,n){var e=c(((t=ya(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(pa(n)/3))),i=Math.pow(10,-r),o=Ma[8+r/3];return function(t){return e(i*t)+o}}}}function Aa(n){return wa=Na(n),t.format=wa.format,t.formatPrefix=wa.formatPrefix,wa}function ka(t){return Math.max(0,-pa(Math.abs(t)))}function Sa(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(pa(n)/3)))-pa(Math.abs(t)))}function Ta(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,pa(n)-pa(t))+1}function Ea(){return new Ca}function Ca(){this.reset()}Aa({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),Ca.prototype={constructor:Ca,reset:function(){this.s=this.t=0},add:function(t){za(Pa,t,this.t),za(this,Pa.s,this.s),this.s?this.t+=Pa.t:this.s=Pa.t},valueOf:function(){return this.s}};var Pa=new Ca;function za(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}var Ra=1e-6,qa=1e-12,Da=Math.PI,La=Da/2,Ua=Da/4,Oa=2*Da,Ya=180/Da,Ba=Da/180,Fa=Math.abs,Ia=Math.atan,Ha=Math.atan2,ja=Math.cos,Xa=Math.ceil,Ga=Math.exp,Va=Math.log,$a=Math.pow,Wa=Math.sin,Za=Math.sign||function(t){return t>0?1:t<0?-1:0},Qa=Math.sqrt,Ja=Math.tan;function Ka(t){return t>1?0:t<-1?Da:Math.acos(t)}function tu(t){return t>1?La:t<-1?-La:Math.asin(t)}function nu(t){return(t=Wa(t/2))*t}function eu(){}function ru(t,n){t&&ou.hasOwnProperty(t.type)&&ou[t.type](t,n)}var iu={Feature:function(t,n){ru(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=ja(n=(n*=Ba)/2+Ua),a=Wa(n),u=du*a,c=hu*o+u*ja(i),f=u*r*Wa(i);pu.add(Ha(f,c)),lu=t,hu=o,du=a}function xu(t){return[Ha(t[1],t[0]),tu(t[2])]}function wu(t){var n=t[0],e=t[1],r=ja(e);return[r*ja(n),r*Wa(n),Wa(e)]}function Mu(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Nu(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Au(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function ku(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Su(t){var n=Qa(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var Tu,Eu,Cu,Pu,zu,Ru,qu,Du,Lu,Uu,Ou,Yu,Bu,Fu,Iu,Hu,ju,Xu,Gu,Vu,$u,Wu,Zu,Qu,Ju,Ku,tc=Ea(),nc={point:ec,lineStart:ic,lineEnd:oc,polygonStart:function(){nc.point=ac,nc.lineStart=uc,nc.lineEnd=cc,tc.reset(),gu.polygonStart()},polygonEnd:function(){gu.polygonEnd(),nc.point=ec,nc.lineStart=ic,nc.lineEnd=oc,pu<0?(Tu=-(Cu=180),Eu=-(Pu=90)):tc>Ra?Pu=90:tc<-Ra&&(Eu=-90),Uu[0]=Tu,Uu[1]=Cu}};function ec(t,n){Lu.push(Uu=[Tu=t,Cu=t]),nPu&&(Pu=n)}function rc(t,n){var e=wu([t*Ba,n*Ba]);if(Du){var r=Nu(Du,e),i=Nu([r[1],-r[0],0],r);Su(i),i=xu(i);var o,a=t-zu,u=a>0?1:-1,c=i[0]*Ya*u,f=Fa(a)>180;f^(u*zuPu&&(Pu=o):f^(u*zu<(c=(c+360)%360-180)&&cPu&&(Pu=n)),f?tfc(Tu,Cu)&&(Cu=t):fc(t,Cu)>fc(Tu,Cu)&&(Tu=t):Cu>=Tu?(tCu&&(Cu=t)):t>zu?fc(Tu,t)>fc(Tu,Cu)&&(Cu=t):fc(t,Cu)>fc(Tu,Cu)&&(Tu=t)}else Lu.push(Uu=[Tu=t,Cu=t]);nPu&&(Pu=n),Du=e,zu=t}function ic(){nc.point=rc}function oc(){Uu[0]=Tu,Uu[1]=Cu,nc.point=ec,Du=null}function ac(t,n){if(Du){var e=t-zu;tc.add(Fa(e)>180?e+(e>0?360:-360):e)}else Ru=t,qu=n;gu.point(t,n),rc(t,n)}function uc(){gu.lineStart()}function cc(){ac(Ru,qu),gu.lineEnd(),Fa(tc)>Ra&&(Tu=-(Cu=180)),Uu[0]=Tu,Uu[1]=Cu,Du=null}function fc(t,n){return(n-=t)<0?n+360:n}function sc(t,n){return t[0]-n[0]}function lc(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nDa?t+Math.round(-t/Oa)*Oa:t,n]}function kc(t,n,e){return(t%=Oa)?n||e?Nc(Tc(t),Ec(n,e)):Tc(t):n||e?Ec(n,e):Ac}function Sc(t){return function(n,e){return[(n+=t)>Da?n-Oa:n<-Da?n+Oa:n,e]}}function Tc(t){var n=Sc(t);return n.invert=Sc(-t),n}function Ec(t,n){var e=ja(t),r=Wa(t),i=ja(n),o=Wa(n);function a(t,n){var a=ja(n),u=ja(t)*a,c=Wa(t)*a,f=Wa(n),s=f*e+u*r;return[Ha(c*i-s*o,u*e-f*r),tu(s*i+c*o)]}return a.invert=function(t,n){var a=ja(n),u=ja(t)*a,c=Wa(t)*a,f=Wa(n),s=f*i-c*o;return[Ha(c*i+f*o,u*e+s*r),tu(s*e-u*r)]},a}function Cc(t){function n(n){return(n=t(n[0]*Ba,n[1]*Ba))[0]*=Ya,n[1]*=Ya,n}return t=kc(t[0]*Ba,t[1]*Ba,t.length>2?t[2]*Ba:0),n.invert=function(n){return(n=t.invert(n[0]*Ba,n[1]*Ba))[0]*=Ya,n[1]*=Ya,n},n}function Pc(t,n,e,r,i,o){if(e){var a=ja(n),u=Wa(n),c=r*e;null==i?(i=n+r*Oa,o=n-c/2):(i=zc(a,i),o=zc(a,o),(r>0?io)&&(i+=r*Oa));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function qc(t,n){return Fa(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function Uc(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,A=N*M,k=A>Da,S=v*x;if(Oc.add(Ha(S*N*Wa(A),g*w+S*ja(A))),a+=k?M+N*Oa:M,k^d>=e^b>=e){var T=Nu(wu(h),wu(_));Su(T);var E=Nu(o,T);Su(E);var C=(k^M>=0?-1:1)*tu(E[2]);(r>C||r===C&&(T[0]||T[1]))&&(u+=k^M>=0?1:-1)}}return(a<-Ra||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(Fc))}return h}}function Fc(t){return t.length>1}function Ic(t,n){return((t=t.x)[0]<0?t[1]-La-Ra:La-t[1])-((n=n.x)[0]<0?n[1]-La-Ra:La-n[1])}var Hc=Bc(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?Da:-Da,c=Fa(o-e);Fa(c-Da)0?La:-La),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=Da&&(Fa(e-i)Ra?Ia((Wa(n)*(o=ja(r))*Wa(e)-Wa(r)*(i=ja(n))*Wa(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*La,r.point(-Da,i),r.point(0,i),r.point(Da,i),r.point(Da,0),r.point(Da,-i),r.point(0,-i),r.point(-Da,-i),r.point(-Da,0),r.point(-Da,i);else if(Fa(t[0]-n[0])>Ra){var o=t[0]0,i=Fa(n)>Ra;function o(t,e){return ja(t)*ja(e)>n}function a(t,e,r){var i=[1,0,0],o=Nu(wu(t),wu(e)),a=Mu(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=Nu(i,o),h=ku(i,f);Au(h,ku(o,s));var d=l,p=Mu(h,d),v=Mu(d,d),g=p*p-v*(Mu(h,h)-1);if(!(g<0)){var y=Qa(g),_=ku(d,(-p-y)/v);if(Au(_,h),_=xu(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(Fa(_[0]-m)Da^(m<=_[0]&&_[0]<=x)){var k=ku(d,(-p+y)/v);return Au(k,h),[_,xu(k)]}}}function u(n,e){var i=r?t:Da-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return Bc(o,function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],v=o(l,h),g=r?v?0:u(l,h):v?u(l+(l<0?Da:-Da),h):0;if(!n&&(f=c=v)&&t.lineStart(),v!==c&&(!(d=a(n,p))||qc(n,d)||qc(p,d))&&(p[0]+=Ra,p[1]+=Ra,v=o(p[0],p[1])),v!==c)s=0,v?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1]),t.lineEnd()),n=d;else if(i&&n&&r^v){var y;g&e||!(y=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&qc(n,p)||t.point(p[0],p[1]),n=p,c=v,e=g},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}},function(n,r,i,o){Pc(o,t,e,i,n,r)},r?[0,-t]:[-Da,t-Da])}var Xc=1e9,Gc=-Xc;function Vc(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return Fa(r[0]-t)0?0:3:Fa(r[0]-e)0?2:1:Fa(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,v,g,y,_,b=a,m=Rc(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);y=!0,g=!1,p=v=NaN},lineEnd:function(){c&&(M(l,h),d&&g&&m.rejoin(),c.push(m.result()));x.point=w,g&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=k(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&Lc(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),y)l=o,h=a,d=u,y=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&g)b.point(o,a);else{var c=[p=Math.max(Gc,Math.min(Xc,p)),v=Math.max(Gc,Math.min(Xc,v))],m=[o=Math.max(Gc,Math.min(Xc,o)),a=Math.max(Gc,Math.min(Xc,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,v=a,g=u}return x}}var $c,Wc,Zc,Qc=Ea(),Jc={sphere:eu,point:eu,lineStart:function(){Jc.point=tf,Jc.lineEnd=Kc},lineEnd:eu,polygonStart:eu,polygonEnd:eu};function Kc(){Jc.point=Jc.lineEnd=eu}function tf(t,n){$c=t*=Ba,Wc=Wa(n*=Ba),Zc=ja(n),Jc.point=nf}function nf(t,n){t*=Ba;var e=Wa(n*=Ba),r=ja(n),i=Fa(t-$c),o=ja(i),a=r*Wa(i),u=Zc*e-Wc*r*o,c=Wc*e+Zc*r*o;Qc.add(Ha(Qa(a*a+u*u),c)),$c=t,Wc=e,Zc=r}function ef(t){return Qc.reset(),cu(t,Jc),+Qc}var rf=[null,null],of={type:"LineString",coordinates:rf};function af(t,n){return rf[0]=t,rf[1]=n,ef(of)}var uf={Feature:function(t,n){return ff(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rRa}).map(c)).concat(g(Xa(o/d)*d,i,d).filter(function(t){return Fa(t%v)>Ra}).map(f))}return _.lines=function(){return b().map(function(t){return{type:"LineString",coordinates:t}})},_.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},_.extent=function(t){return arguments.length?_.extentMajor(t).extentMinor(t):_.extentMinor()},_.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),_.precision(y)):[[r,u],[e,a]]},_.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),_.precision(y)):[[n,o],[t,i]]},_.step=function(t){return arguments.length?_.stepMajor(t).stepMinor(t):_.stepMinor()},_.stepMajor=function(t){return arguments.length?(p=+t[0],v=+t[1],_):[p,v]},_.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],_):[h,d]},_.precision=function(h){return arguments.length?(y=+h,c=vf(o,i,90),f=gf(n,t,y),s=vf(u,a,90),l=gf(r,e,y),_):y},_.extentMajor([[-180,-90+Ra],[180,90-Ra]]).extentMinor([[-180,-80-Ra],[180,80+Ra]])}function _f(t){return t}var bf,mf,xf,wf,Mf=Ea(),Nf=Ea(),Af={point:eu,lineStart:eu,lineEnd:eu,polygonStart:function(){Af.lineStart=kf,Af.lineEnd=Ef},polygonEnd:function(){Af.lineStart=Af.lineEnd=Af.point=eu,Mf.add(Fa(Nf)),Nf.reset()},result:function(){var t=Mf/2;return Mf.reset(),t}};function kf(){Af.point=Sf}function Sf(t,n){Af.point=Tf,bf=xf=t,mf=wf=n}function Tf(t,n){Nf.add(wf*t-xf*n),xf=t,wf=n}function Ef(){Tf(bf,mf)}var Cf=1/0,Pf=Cf,zf=-Cf,Rf=zf,qf={point:function(t,n){tzf&&(zf=t);nRf&&(Rf=n)},lineStart:eu,lineEnd:eu,polygonStart:eu,polygonEnd:eu,result:function(){var t=[[Cf,Pf],[zf,Rf]];return zf=Rf=-(Pf=Cf=1/0),t}};var Df,Lf,Uf,Of,Yf=0,Bf=0,Ff=0,If=0,Hf=0,jf=0,Xf=0,Gf=0,Vf=0,$f={point:Wf,lineStart:Zf,lineEnd:Kf,polygonStart:function(){$f.lineStart=ts,$f.lineEnd=ns},polygonEnd:function(){$f.point=Wf,$f.lineStart=Zf,$f.lineEnd=Kf},result:function(){var t=Vf?[Xf/Vf,Gf/Vf]:jf?[If/jf,Hf/jf]:Ff?[Yf/Ff,Bf/Ff]:[NaN,NaN];return Yf=Bf=Ff=If=Hf=jf=Xf=Gf=Vf=0,t}};function Wf(t,n){Yf+=t,Bf+=n,++Ff}function Zf(){$f.point=Qf}function Qf(t,n){$f.point=Jf,Wf(Uf=t,Of=n)}function Jf(t,n){var e=t-Uf,r=n-Of,i=Qa(e*e+r*r);If+=i*(Uf+t)/2,Hf+=i*(Of+n)/2,jf+=i,Wf(Uf=t,Of=n)}function Kf(){$f.point=Wf}function ts(){$f.point=es}function ns(){rs(Df,Lf)}function es(t,n){$f.point=rs,Wf(Df=Uf=t,Lf=Of=n)}function rs(t,n){var e=t-Uf,r=n-Of,i=Qa(e*e+r*r);If+=i*(Uf+t)/2,Hf+=i*(Of+n)/2,jf+=i,Xf+=(i=Of*t-Uf*n)*(Uf+t),Gf+=i*(Of+n),Vf+=3*i,Wf(Uf=t,Of=n)}function is(t){this._context=t}is.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,Oa)}},result:eu};var os,as,us,cs,fs,ss=Ea(),ls={point:eu,lineStart:function(){ls.point=hs},lineEnd:function(){os&&ds(as,us),ls.point=eu},polygonStart:function(){os=!0},polygonEnd:function(){os=null},result:function(){var t=+ss;return ss.reset(),t}};function hs(t,n){ls.point=ds,as=cs=t,us=fs=n}function ds(t,n){cs-=t,fs-=n,ss.add(Qa(cs*cs+fs*fs)),cs=t,fs=n}function ps(){this._string=[]}function vs(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function gs(t){return function(n){var e=new ys;for(var r in t)e[r]=t[r];return e.stream=n,e}}function ys(){}function _s(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),cu(e,t.stream(qf)),n(qf.result()),null!=r&&t.clipExtent(r),t}function bs(t,n,e){return _s(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function ms(t,n,e){return bs(t,[[0,0],n],e)}function xs(t,n,e){return _s(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function ws(t,n,e){return _s(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}ps.prototype={_radius:4.5,_circle:vs(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=vs(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},ys.prototype={constructor:ys,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Ms=16,Ns=ja(30*Ba);function As(t,n){return+n?function(t,n){function e(r,i,o,a,u,c,f,s,l,h,d,p,v,g){var y=f-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=c+p,M=Qa(m*m+x*x+w*w),N=tu(w/=M),A=Fa(Fa(w)-1)n||Fa((y*E+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*Ba:0,S()):[g*Ya,y*Ya,_*Ya]},A.angle=function(t){return arguments.length?(b=t%360*Ba,S()):b*Ya},A.precision=function(t){return arguments.length?(a=As(u,N=t*t),T()):Qa(N)},A.fitExtent=function(t,n){return bs(A,t,n)},A.fitSize=function(t,n){return ms(A,t,n)},A.fitWidth=function(t,n){return xs(A,t,n)},A.fitHeight=function(t,n){return ws(A,t,n)},function(){return n=t.apply(this,arguments),A.invert=n.invert&&k,S()}}function Cs(t){var n=0,e=Da/3,r=Es(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*Ba,e=t[1]*Ba):[n*Ya,e*Ya]},i}function Ps(t,n){var e=Wa(t),r=(e+Wa(n))/2;if(Fa(r)0?n<-La+Ra&&(n=-La+Ra):n>La-Ra&&(n=La-Ra);var e=i/$a(Bs(n),r);return[e*Wa(r*t),i-e*ja(r*t)]}return o.invert=function(t,n){var e=i-n,o=Za(r)*Qa(t*t+e*e);return[Ha(t,Fa(e))/r*Za(e),2*Ia($a(i/o,1/r))-La]},o}function Is(t,n){return[t,n]}function Hs(t,n){var e=ja(t),r=t===n?Wa(t):(e-ja(n))/(n-t),i=e/r+t;if(Fa(r)=0;)n+=e[r].value;else n=1;t.value=n}function al(t,n){var e,r,i,o,a,u=new sl(t),c=+t.value&&(u.value=t.value),f=[u];for(null==n&&(n=ul);e=f.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)f.push(r=e.children[o]=new sl(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(fl)}function ul(t){return t.children}function cl(t){t.data=t.data.data}function fl(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function sl(t){this.data=t,this.depth=this.height=0,this.parent=null}Ws.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(js+Xs*i+o*(Gs+Vs*i))-n)/(js+3*Xs*i+o*(7*Gs+9*Vs*i)))*r)*i*i,!(Fa(e)Ra&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Ks.invert=Ds(tu),tl.invert=Ds(function(t){return 2*Ia(t)}),nl.invert=function(t,n){return[-n,2*Ia(Ga(t))-La]},sl.prototype=al.prototype={constructor:sl,count:function(){return this.eachAfter(ol)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return al(this).eachBefore(cl)}};var ll=Array.prototype.slice;function hl(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(ll.call(t))).length,o=[];r0&&e*e>r*r+i*i}function gl(t,n){for(var e=0;e(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function xl(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function wl(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function Ml(t){this._=t,this.next=null,this.previous=null}function Nl(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,c,f,s,l;if((n=t[0]).x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;ml(e,n,r=t[2]),n=new Ml(n),e=new Ml(e),r=new Ml(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:c1?n:1)},e}(Gl);var Wl=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Gl);function Zl(t,n){return t[0]-n[0]||t[1]-n[1]}function Ql(t){for(var n,e,r,i=t.length,o=[0,1],a=2,u=2;u1&&(n=t[o[a-2]],e=t[o[a-1]],r=t[u],(e[0]-n[0])*(r[1]-n[1])-(e[1]-n[1])*(r[0]-n[0])<=0);)--a;o[a++]=u}return o.slice(0,a)}function Jl(){return Math.random()}var Kl=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(Jl),th=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(Jl),nh=function t(n){function e(){var t=th.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(Jl),eh=function t(n){function e(t){return function(){for(var e=0,r=0;rr&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function _h(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?bh:_h,i=o=null,l}function l(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),he)))(e)))},l.domain=function(t){return arguments.length?(a=ch.call(t,dh),f===vh||(f=yh(a)),s()):a.slice()},l.range=function(t){return arguments.length?(u=fh.call(t),s()):u.slice()},l.rangeRound=function(t){return u=fh.call(t),c=_e,s()},l.clamp=function(t){return arguments.length?(f=t?yh(a):vh,l):f!==vh},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function wh(t,n){return xh()(t,n)}function Mh(n,e,r,i){var o,a=w(n,e,r);switch((i=ya(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=Sa(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Ta(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=ka(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Nh(t){var n=t.domain;return t.ticks=function(t){var e=n();return m(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Mh(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i=n(),o=0,a=i.length-1,u=i[o],c=i[a];return c0?r=x(u=Math.floor(u/r)*r,c=Math.ceil(c/r)*r,e):r<0&&(r=x(u=Math.ceil(u*r)/r,c=Math.floor(c*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(c/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(c*r)/r,n(i)),t},t}function Ah(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;hc)break;v.push(l)}}else for(;h=1;--s)if(!((l=f*s)c)break;v.push(l)}}else v=m(h,d,Math.min(d-h,p)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*a0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Fh.setTime(+n),Ih.setTime(+r),t(Fh),t(Ih),Math.floor(e(Fh,Ih))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}var jh=Hh(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});jh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?Hh(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):jh:null};var Xh=jh.range,Gh=6e4,Vh=6048e5,$h=Hh(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Wh=$h.range,Zh=Hh(function(t){t.setTime(Math.floor(t/Gh)*Gh)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getMinutes()}),Qh=Zh.range,Jh=Hh(function(t){var n=t.getTimezoneOffset()*Gh%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),Kh=Jh.range,td=Hh(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/864e5},function(t){return t.getDate()-1}),nd=td.range;function ed(t){return Hh(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/Vh})}var rd=ed(0),id=ed(1),od=ed(2),ad=ed(3),ud=ed(4),cd=ed(5),fd=ed(6),sd=rd.range,ld=id.range,hd=od.range,dd=ad.range,pd=ud.range,vd=cd.range,gd=fd.range,yd=Hh(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),_d=yd.range,bd=Hh(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});bd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Hh(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var md=bd.range,xd=Hh(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getUTCMinutes()}),wd=xd.range,Md=Hh(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),Nd=Md.range,Ad=Hh(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),kd=Ad.range;function Sd(t){return Hh(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Vh})}var Td=Sd(0),Ed=Sd(1),Cd=Sd(2),Pd=Sd(3),zd=Sd(4),Rd=Sd(5),qd=Sd(6),Dd=Td.range,Ld=Ed.range,Ud=Cd.range,Od=Pd.range,Yd=zd.range,Bd=Rd.range,Fd=qd.range,Id=Hh(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),Hd=Id.range,jd=Hh(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});jd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Hh(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Xd=jd.range;function Gd(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Vd(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function $d(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function Wd(t){var n=t.dateTime,e=t.date,r=t.time,i=t.periods,o=t.days,a=t.shortDays,u=t.months,c=t.shortMonths,f=rp(i),s=ip(i),l=rp(o),h=ip(o),d=rp(a),p=ip(a),v=rp(u),g=ip(u),y=rp(c),_=ip(c),b={a:function(t){return a[t.getDay()]},A:function(t){return o[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return u[t.getMonth()]},c:null,d:Np,e:Np,f:Ep,H:Ap,I:kp,j:Sp,L:Tp,m:Cp,M:Pp,p:function(t){return i[+(t.getHours()>=12)]},Q:ov,s:av,S:zp,u:Rp,U:qp,V:Dp,w:Lp,W:Up,x:null,X:null,y:Op,Y:Yp,Z:Bp,"%":iv},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:Fp,e:Fp,f:Gp,H:Ip,I:Hp,j:jp,L:Xp,m:Vp,M:$p,p:function(t){return i[+(t.getUTCHours()>=12)]},Q:ov,s:av,S:Wp,u:Zp,U:Qp,V:Jp,w:Kp,W:tv,x:null,X:null,y:nv,Y:ev,Z:rv,"%":iv},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=y.exec(n.slice(e));return r?(t.m=_[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=g[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,e,r){return N(t,n,e,r)},d:pp,e:pp,f:mp,H:gp,I:gp,j:vp,L:bp,m:dp,M:yp,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s[r[0].toLowerCase()],e+r[0].length):-1},Q:wp,s:Mp,S:_p,u:ap,U:up,V:cp,w:op,W:fp,x:function(t,n,r){return N(t,e,n,r)},X:function(t,n,e){return N(t,r,n,e)},y:lp,Y:sp,Z:hp,"%":xp};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=Vd($d(o.y))).getUTCDay(),r=i>4||0===i?Ed.ceil(r):Ed(r),r=Ad.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=n($d(o.y))).getDay(),r=i>4||0===i?id.ceil(r):id(r),r=td.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?Vd($d(o.y)).getUTCDay():n($d(o.y)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,Vd(o)):n(o)}}function N(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in Qd?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",Gd);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t,Vd);return n.toString=function(){return t},n}}}var Zd,Qd={"-":"",_:" ",0:"0"},Jd=/^\s*\d+/,Kd=/^%/,tp=/[\\^$*+?|[\]().{}]/g;function np(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function hp(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function dp(t,n,e){var r=Jd.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function pp(t,n,e){var r=Jd.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function vp(t,n,e){var r=Jd.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function gp(t,n,e){var r=Jd.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function yp(t,n,e){var r=Jd.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function _p(t,n,e){var r=Jd.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function bp(t,n,e){var r=Jd.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function mp(t,n,e){var r=Jd.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function xp(t,n,e){var r=Kd.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function wp(t,n,e){var r=Jd.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Mp(t,n,e){var r=Jd.exec(n.slice(e));return r?(t.Q=1e3*+r[0],e+r[0].length):-1}function Np(t,n){return np(t.getDate(),n,2)}function Ap(t,n){return np(t.getHours(),n,2)}function kp(t,n){return np(t.getHours()%12||12,n,2)}function Sp(t,n){return np(1+td.count(bd(t),t),n,3)}function Tp(t,n){return np(t.getMilliseconds(),n,3)}function Ep(t,n){return Tp(t,n)+"000"}function Cp(t,n){return np(t.getMonth()+1,n,2)}function Pp(t,n){return np(t.getMinutes(),n,2)}function zp(t,n){return np(t.getSeconds(),n,2)}function Rp(t){var n=t.getDay();return 0===n?7:n}function qp(t,n){return np(rd.count(bd(t),t),n,2)}function Dp(t,n){var e=t.getDay();return t=e>=4||0===e?ud(t):ud.ceil(t),np(ud.count(bd(t),t)+(4===bd(t).getDay()),n,2)}function Lp(t){return t.getDay()}function Up(t,n){return np(id.count(bd(t),t),n,2)}function Op(t,n){return np(t.getFullYear()%100,n,2)}function Yp(t,n){return np(t.getFullYear()%1e4,n,4)}function Bp(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+np(n/60|0,"0",2)+np(n%60,"0",2)}function Fp(t,n){return np(t.getUTCDate(),n,2)}function Ip(t,n){return np(t.getUTCHours(),n,2)}function Hp(t,n){return np(t.getUTCHours()%12||12,n,2)}function jp(t,n){return np(1+Ad.count(jd(t),t),n,3)}function Xp(t,n){return np(t.getUTCMilliseconds(),n,3)}function Gp(t,n){return Xp(t,n)+"000"}function Vp(t,n){return np(t.getUTCMonth()+1,n,2)}function $p(t,n){return np(t.getUTCMinutes(),n,2)}function Wp(t,n){return np(t.getUTCSeconds(),n,2)}function Zp(t){var n=t.getUTCDay();return 0===n?7:n}function Qp(t,n){return np(Td.count(jd(t),t),n,2)}function Jp(t,n){var e=t.getUTCDay();return t=e>=4||0===e?zd(t):zd.ceil(t),np(zd.count(jd(t),t)+(4===jd(t).getUTCDay()),n,2)}function Kp(t){return t.getUTCDay()}function tv(t,n){return np(Ed.count(jd(t),t),n,2)}function nv(t,n){return np(t.getUTCFullYear()%100,n,2)}function ev(t,n){return np(t.getUTCFullYear()%1e4,n,4)}function rv(){return"+0000"}function iv(){return"%"}function ov(t){return+t}function av(t){return Math.floor(+t/1e3)}function uv(n){return Zd=Wd(n),t.timeFormat=Zd.format,t.timeParse=Zd.parse,t.utcFormat=Zd.utcFormat,t.utcParse=Zd.utcParse,Zd}uv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var cv=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");var fv=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),sv=1e3,lv=60*sv,hv=60*lv,dv=24*hv,pv=7*dv,vv=30*dv,gv=365*dv;function yv(t){return new Date(t)}function _v(t){return t instanceof Date?+t:+new Date(+t)}function bv(t,n,r,i,o,a,u,c,f){var s=wh(vh,vh),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),v=f("%I:%M"),g=f("%I %p"),y=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y"),x=[[u,1,sv],[u,5,5*sv],[u,15,15*sv],[u,30,30*sv],[a,1,lv],[a,5,5*lv],[a,15,15*lv],[a,30,30*lv],[o,1,hv],[o,3,3*hv],[o,6,6*hv],[o,12,12*hv],[i,1,dv],[i,2,2*dv],[r,1,pv],[n,1,vv],[n,3,3*vv],[t,1,gv]];function M(e){return(u(e)=1?iy:t<=-1?-iy:Math.asin(t)}function uy(t){return t.innerRadius}function cy(t){return t.outerRadius}function fy(t){return t.startAngle}function sy(t){return t.endAngle}function ly(t){return t&&t.padAngle}function hy(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/ny(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*ny(Jg(0,x*x*m-w*w)),N=(w*b-_*M)/m,A=(-w*_-b*M)/m,k=(w*b+_*M)/m,S=(-w*_+b*M)/m,T=N-g,E=A-y,C=k-g,P=S-y;return T*T+E*E>C*C+P*P&&(N=k,A=S),{cx:N,cy:A,x01:-s,y01:-l,x11:N*(i/x-1),y11:A*(i/x-1)}}function dy(t){this._context=t}function py(t){return new dy(t)}function vy(t){return t[0]}function gy(t){return t[1]}function yy(){var t=vy,n=gy,e=$g(!0),r=null,i=py,o=null;function a(a){var u,c,f,s=a.length,l=!1;for(null==r&&(o=i(f=ji())),u=0;u<=s;++u)!(u=s;--l)u.point(g[l],y[l]);u.lineEnd(),u.areaEnd()}v&&(g[f]=+t(h,f,c),y[f]=+e(h,f,c),u.point(n?+n(h,f,c):g[f],r?+r(h,f,c):y[f]))}if(d)return u=null,d+""||null}function f(){return yy().defined(i).curve(a).context(o)}return c.x=function(e){return arguments.length?(t="function"==typeof e?e:$g(+e),n=null,c):t},c.x0=function(n){return arguments.length?(t="function"==typeof n?n:$g(+n),c):t},c.x1=function(t){return arguments.length?(n=null==t?null:"function"==typeof t?t:$g(+t),c):n},c.y=function(t){return arguments.length?(e="function"==typeof t?t:$g(+t),r=null,c):e},c.y0=function(t){return arguments.length?(e="function"==typeof t?t:$g(+t),c):e},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:$g(+t),c):r},c.lineX0=c.lineY0=function(){return f().x(t).y(e)},c.lineY1=function(){return f().x(t).y(r)},c.lineX1=function(){return f().x(n).y(e)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:$g(!!t),c):i},c.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),c):a},c.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),c):o},c}function by(t,n){return nt?1:n>=t?0:NaN}function my(t){return t}dy.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var xy=My(py);function wy(t){this._curve=t}function My(t){function n(n){return new wy(t(n))}return n._curve=t,n}function Ny(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(My(t)):n()._curve},t}function Ay(){return Ny(yy().curve(xy))}function ky(){var t=_y().curve(xy),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Ny(e())},delete t.lineX0,t.lineEndAngle=function(){return Ny(r())},delete t.lineX1,t.lineInnerRadius=function(){return Ny(i())},delete t.lineY0,t.lineOuterRadius=function(){return Ny(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(My(t)):n()._curve},t}function Sy(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}wy.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Ty=Array.prototype.slice;function Ey(t){return t.source}function Cy(t){return t.target}function Py(t){var n=Ey,e=Cy,r=vy,i=gy,o=null;function a(){var a,u=Ty.call(arguments),c=n.apply(this,u),f=e.apply(this,u);if(o||(o=a=ji()),t(o,+r.apply(this,(u[0]=c,u)),+i.apply(this,u),+r.apply(this,(u[0]=f,u)),+i.apply(this,u)),a)return o=null,a+""||null}return a.source=function(t){return arguments.length?(n=t,a):n},a.target=function(t){return arguments.length?(e=t,a):e},a.x=function(t){return arguments.length?(r="function"==typeof t?t:$g(+t),a):r},a.y=function(t){return arguments.length?(i="function"==typeof t?t:$g(+t),a):i},a.context=function(t){return arguments.length?(o=null==t?null:t,a):o},a}function zy(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function Ry(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function qy(t,n,e,r,i){var o=Sy(n,e),a=Sy(n,e=(e+i)/2),u=Sy(r,e),c=Sy(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],c[0],c[1])}var Dy={draw:function(t,n){var e=Math.sqrt(n/ry);t.moveTo(e,0),t.arc(0,0,e,0,oy)}},Ly={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Uy=Math.sqrt(1/3),Oy=2*Uy,Yy={draw:function(t,n){var e=Math.sqrt(n/Oy),r=e*Uy;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},By=Math.sin(ry/10)/Math.sin(7*ry/10),Fy=Math.sin(oy/10)*By,Iy=-Math.cos(oy/10)*By,Hy={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Fy*e,i=Iy*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=oy*o/5,u=Math.cos(a),c=Math.sin(a);t.lineTo(c*e,-u*e),t.lineTo(u*r-c*i,c*r+u*i)}t.closePath()}},jy={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Xy=Math.sqrt(3),Gy={draw:function(t,n){var e=-Math.sqrt(n/(3*Xy));t.moveTo(0,2*e),t.lineTo(-Xy*e,-e),t.lineTo(Xy*e,-e),t.closePath()}},Vy=Math.sqrt(3)/2,$y=1/Math.sqrt(12),Wy=3*($y/2+1),Zy={draw:function(t,n){var e=Math.sqrt(n/Wy),r=e/2,i=e*$y,o=r,a=e*$y+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(-.5*r-Vy*i,Vy*r+-.5*i),t.lineTo(-.5*o-Vy*a,Vy*o+-.5*a),t.lineTo(-.5*u-Vy*c,Vy*u+-.5*c),t.lineTo(-.5*r+Vy*i,-.5*i-Vy*r),t.lineTo(-.5*o+Vy*a,-.5*a-Vy*o),t.lineTo(-.5*u+Vy*c,-.5*c-Vy*u),t.closePath()}},Qy=[Dy,Ly,Yy,jy,Hy,Gy,Zy];function Jy(){}function Ky(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function t_(t){this._context=t}function n_(t){this._context=t}function e_(t){this._context=t}function r_(t,n){this._basis=new t_(t),this._beta=n}t_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Ky(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Ky(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},n_.prototype={areaStart:Jy,areaEnd:Jy,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Ky(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},e_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Ky(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},r_.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var i_=function t(n){function e(t){return 1===n?new t_(t):new r_(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function o_(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function a_(t,n){this._context=t,this._k=(1-n)/6}a_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:o_(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:o_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var u_=function t(n){function e(t){return new a_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function c_(t,n){this._context=t,this._k=(1-n)/6}c_.prototype={areaStart:Jy,areaEnd:Jy,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:o_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var f_=function t(n){function e(t){return new c_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function s_(t,n){this._context=t,this._k=(1-n)/6}s_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:o_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var l_=function t(n){function e(t){return new s_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function h_(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>ey){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>ey){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function d_(t,n){this._context=t,this._alpha=n}d_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:h_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var p_=function t(n){function e(t){return n?new d_(t,n):new a_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function v_(t,n){this._context=t,this._alpha=n}v_.prototype={areaStart:Jy,areaEnd:Jy,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:h_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var g_=function t(n){function e(t){return n?new v_(t,n):new c_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function y_(t,n){this._context=t,this._alpha=n}y_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:h_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var __=function t(n){function e(t){return n?new y_(t,n):new s_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function b_(t){this._context=t}function m_(t){return t<0?-1:1}function x_(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(m_(o)+m_(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function w_(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function M_(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function N_(t){this._context=t}function A_(t){this._context=new k_(t)}function k_(t){this._context=t}function S_(t){this._context=t}function T_(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function z_(t,n){return t[n]}function R_(t){var n=t.map(q_);return P_(t).sort(function(t,e){return n[t]-n[e]})}function q_(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function D_(t){var n=t.map(L_);return P_(t).sort(function(t,e){return n[t]-n[e]})}function L_(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-c,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-f,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[c+s*h,f+s*d]),l<1&&(t[1]=[c+l*h,f+l*d]),!0)}}}}}function W_(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,c=t[0],f=t.left,s=t.right,l=f[0],h=f[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>d){if(c){if(c[1]>=i)return}else c=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(c){if(c[1]=r)return}else c=[n,a*n+u];o=[r,a*r+u]}else{if(c){if(c[0]=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},B_.prototype={constructor:B_,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=j_(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(I_(this,e),e=(t=e).U),e.C=!1,r.C=!0,H_(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(H_(this,e),e=(t=e).U),e.C=!1,r.C=!0,I_(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?j_(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,I_(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,H_(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,I_(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,H_(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,I_(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,H_(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var K_,tb=[];function nb(){F_(this),this.x=this.y=this.arc=this.site=this.cy=null}function eb(t){var n=t.P,e=t.N;if(n&&e){var r=n.site,i=t.site,o=e.site;if(r!==o){var a=i[0],u=i[1],c=r[0]-a,f=r[1]-u,s=o[0]-a,l=o[1]-u,h=2*(c*l-f*s);if(!(h>=-yb)){var d=c*c+f*f,p=s*s+l*l,v=(l*d-f*p)/h,g=(c*p-s*d)/h,y=tb.pop()||new nb;y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=pb._;b;)if(y.ygb)u=u.L;else{if(!((i=o-lb(u,a))>gb)){r>-gb?(n=u.P,e=u):i>-gb?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}!function(t){db[t.index]={site:t,halfedges:[]}}(t);var c=ab(t);if(hb.insert(n,c),n||e){if(n===e)return rb(n),e=ab(n.site),hb.insert(c,e),c.edge=e.edge=X_(n.site,c.site),eb(n),void eb(e);if(e){rb(n),rb(e);var f=n.site,s=f[0],l=f[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];V_(e.edge,f,p,m),c.edge=X_(f,t,null,m),e.edge=X_(t,p,null,m),eb(n),eb(e)}else c.edge=X_(n.site,c.site)}}function sb(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],c=e[1],f=c-n;if(!f)return u;var s=u-r,l=1/o-1/f,h=s/f;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*f)-c+f/2+i-o/2)))/l+r:(r+u)/2}function lb(t,n){var e=t.N;if(e)return sb(e,n);var r=t.site;return r[1]===n?r[0]:1/0}var hb,db,pb,vb,gb=1e-6,yb=1e-12;function _b(t,n){return n[1]-t[1]||n[0]-t[0]}function bb(t,n){var e,r,i,o=t.sort(_b).pop();for(vb=[],db=new Array(t.length),hb=new B_,pb=new B_;;)if(i=K_,o&&(!i||o[1]gb||Math.abs(i[0][1]-i[1][1])>gb)||delete vb[o]}(a,u,c,f),function(t,n,e,r){var i,o,a,u,c,f,s,l,h,d,p,v,g=db.length,y=!0;for(i=0;igb||Math.abs(v-h)>gb)&&(c.splice(u,0,vb.push(G_(a,d,Math.abs(p-t)gb?[t,Math.abs(l-t)gb?[Math.abs(h-r)gb?[e,Math.abs(l-e)gb?[Math.abs(h-n)=u)return null;var c=t-i.site[0],f=n-i.site[1],s=c*c+f*f;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var c=t-u[0],f=n-u[1],l=c*c+f*f;lr?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Nb.prototype=wb.prototype,t.version="5.8.0",t.bisect=i,t.bisectRight=i,t.bisectLeft=o,t.ascending=n,t.bisector=e,t.cross=function(t,n,e){var r,i,o,u,c=t.length,f=n.length,s=new Array(c*f);for(null==e&&(e=a),r=o=0;rt?1:n>=t?0:NaN},t.deviation=f,t.extent=s,t.histogram=function(){var t=v,n=s,e=M;function r(r){var o,a,u=r.length,c=new Array(u);for(o=0;ol;)h.pop(),--d;var p,v=new Array(d+1);for(o=0;o<=d;++o)(p=v[o]=[]).x0=o>0?h[o-1]:s,p.x1=o=r.length)return null!=t&&e.sort(t),null!=n?n(e):e;for(var c,f,s,l=-1,h=e.length,d=r[i++],p=Qi(),v=a();++lr.length)return e;var a,u=i[o-1];return null!=n&&o>=r.length?a=e.entries():(a=[],e.each(function(n,e){a.push({key:e,values:t(n,o)})})),null!=u?a.sort(function(t,n){return u(t.key,n.key)}):a}(o(t,0,to,no),0)},key:function(t){return r.push(t),e},sortKeys:function(t){return i[r.length-1]=t,e},sortValues:function(n){return t=n,e},rollup:function(t){return n=t,e}}},t.set=io,t.map=Qi,t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.entries=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},t.color=hn,t.rgb=gn,t.hsl=mn,t.lab=Rn,t.hcl=Bn,t.lch=function(t,n,e,r){return 1===arguments.length?Yn(t):new Fn(e,n,t,null==r?1:r)},t.gray=function(t,n){return new qn(t,0,0,null==n?1:n)},t.cubehelix=Zn,t.contours=po,t.contourDensity=function(){var t=yo,n=_o,e=bo,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=uo(20);function l(r){var i=new Float32Array(c*f),l=new Float32Array(c*f);r.forEach(function(r,o,s){var l=+t(r,o,s)+u>>a,h=+n(r,o,s)+u>>a,d=+e(r,o,s);l>=0&&l=0&&h>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),vo({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),vo({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a);var d=s(i);if(!Array.isArray(d)){var p=A(i);d=w(0,p,d),(d=g(0,Math.floor(p/d)*d,d)).shift()}return po().thresholds(d).size([c,f])(i).map(h)}function h(t){return t.value*=Math.pow(2,-2*a),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function y(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,l}return l.x=function(n){return arguments.length?(t="function"==typeof n?n:uo(+n),l):t},l.y=function(t){return arguments.length?(n="function"==typeof t?t:uo(+t),l):n},l.weight=function(t){return arguments.length?(e="function"==typeof t?t:uo(+t),l):e},l.size=function(t){if(!arguments.length)return[r,i];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return r=n,i=e,y()},l.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),y()},l.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?uo(oo.call(t)):uo(t),l):s},l.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=Math.round((Math.sqrt(4*t*t+1)-1)/2),y()},l},t.dispatch=I,t.drag=function(){var n,e,r,i,o=Gt,a=Vt,u=$t,c=Wt,f={},s=I("start","drag","end"),l=0,h=0;function d(t){t.on("mousedown.drag",p).filter(c).on("touchstart.drag",y).on("touchmove.drag",_).on("touchend.drag touchcancel.drag",b).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function p(){if(!i&&o.apply(this,arguments)){var u=m("mouse",a.apply(this,arguments),Ot,this,arguments);u&&(zt(t.event.view).on("mousemove.drag",v,!0).on("mouseup.drag",g,!0),It(t.event.view),Bt(),r=!1,n=t.event.clientX,e=t.event.clientY,u("start"))}}function v(){if(Ft(),!r){var i=t.event.clientX-n,o=t.event.clientY-e;r=i*i+o*o>h}f.mouse("drag")}function g(){zt(t.event.view).on("mousemove.drag mouseup.drag",null),Ht(t.event.view,r),Ft(),f.mouse("end")}function y(){if(o.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=a.apply(this,arguments),u=r.length;for(n=0;nc+d||if+d||ou.index){var p=c-a.x-a.vx,v=f-a.y-a.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function u(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r=a)){(t.data!==n||t.next)&&(0===s&&(d+=(s=Zo())*s),0===l&&(d+=(l=Zo())*l),d1?(null==e?u.remove(t):u.set(t,d(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=Wo(.1);function o(t){for(var i,o=0,a=n.length;ofc(r[0],r[1])&&(r[1]=i[1]),fc(i[0],r[1])>fc(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=fc(r[1],i[0]))>a&&(a=u,Tu=i[0],Cu=r[1])}return Lu=Uu=null,Tu===1/0||Eu===1/0?[[NaN,NaN],[NaN,NaN]]:[[Tu,Eu],[Cu,Pu]]},t.geoCentroid=function(t){Ou=Yu=Bu=Fu=Iu=Hu=ju=Xu=Gu=Vu=$u=0,cu(t,hc);var n=Gu,e=Vu,r=$u,i=n*n+e*e+r*r;return i=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++e2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=nl,t.geoRotation=Cc,t.geoStream=cu,t.geoTransform=function(t){return{stream:gs(t)}},t.cluster=function(){var t=el,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter(function(n){var e=n.children;e?(n.x=function(t){return t.reduce(rl,0)/t.length}(e),n.y=function(t){return 1+t.reduce(il,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)});var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.hierarchy=al,t.pack=function(){var t=null,n=1,e=1,r=kl;function i(i){return i.x=n/2,i.y=e/2,t?i.eachBefore(El(t)).eachAfter(Cl(r,.5)).eachBefore(Pl(1)):i.eachBefore(El(Tl)).eachAfter(Cl(kl,1)).eachAfter(Cl(r,i.r/Math.min(n,e))).eachBefore(Pl(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=null==(e=n)?null:Al(e),i):t;var e},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:Sl(+t),i):r},i},t.packSiblings=function(t){return Nl(t),t},t.packEnclose=hl,t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Rl(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0)throw new Error("cycle");return o}return e.id=function(n){return arguments.length?(t=Al(n),e):t},e.parentId=function(t){return arguments.length?(n=Al(t),e):n},e},t.tree=function(){var t=Yl,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new jl(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new jl(r[i],i)),e.parent=n;return(a.parent=new jl(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),v=e/(l.depth||1);i.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Fl(u),o=Bl(o),u&&o;)c=Bl(c),(a=Fl(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Il(Hl(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Fl(a)&&(a.t=u,a.m+=l-s),o&&!Bl(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=$l,n=!1,e=1,r=1,i=[0],o=kl,a=kl,u=kl,c=kl,f=kl;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(zl),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}for(var l=f[n],h=r/2+l,d=n+1,p=e-1;d>>1;f[v]c-o){var _=(i*y+a*g)/r;t(n,d,g,i,o,_,c),t(d,e,y,_,o,a,c)}else{var b=(o*y+c*g)/r;t(n,d,g,i,o,a,b),t(d,e,y,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Rl,t.treemapSlice=Xl,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Xl:Rl)(t,n,e,r,i)},t.treemapSquarify=$l,t.treemapResquarify=Wl,t.interpolate=ye,t.interpolateArray=se,t.interpolateBasis=Kn,t.interpolateBasisClosed=te,t.interpolateDate=le,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateHue=function(t,n){var e=re(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateNumber=he,t.interpolateObject=de,t.interpolateRound=_e,t.interpolateString=ge,t.interpolateTransformCss=Se,t.interpolateTransformSvg=Te,t.interpolateZoom=qe,t.interpolateRgb=ae,t.interpolateRgbBasis=ce,t.interpolateRgbBasisClosed=fe,t.interpolateHsl=Le,t.interpolateHslLong=Ue,t.interpolateLab=function(t,n){var e=oe((t=Rn(t)).l,(n=Rn(n)).l),r=oe(t.a,n.a),i=oe(t.b,n.b),o=oe(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateHcl=Ye,t.interpolateHclLong=Be,t.interpolateCubehelix=Ie,t.interpolateCubehelixLong=He,t.piecewise=function(t,n){for(var e=0,r=n.length-1,i=n[0],o=new Array(r<0?0:r);e=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;nu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonLength=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],a=o[0],u=o[1],c=0;++r0?a[n-1]:r[0],n=o?[a[o-1],r]:[a[n-1],a[n]]},c.unknown=function(t){return arguments.length?(n=t,c):c},c.thresholds=function(){return a.slice()},c.copy=function(){return t().domain([e,r]).range(u).unknown(n)},oh.apply(Nh(c),arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],o=1;function a(t){return t<=t?r[i(e,t,0,o)]:n}return a.domain=function(t){return arguments.length?(e=fh.call(t),o=Math.min(e.length,r.length-1),a):e.slice()},a.range=function(t){return arguments.length?(r=fh.call(t),o=Math.min(e.length,r.length-1),a):r.slice()},a.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},a.unknown=function(t){return arguments.length?(n=t,a):n},a.copy=function(){return t().domain(e).range(r).unknown(n)},oh.apply(a,arguments)},t.scaleTime=function(){return oh.apply(bv(bd,yd,rd,td,Jh,Zh,$h,jh,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return oh.apply(bv(jd,Id,Td,Ad,Md,xd,$h,jh,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scaleSequential=function t(){var n=Nh(mv()(vh));return n.copy=function(){return xv(n,t())},ah.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=zh(mv()).domain([1,10]);return n.copy=function(){return xv(n,t()).base(n.base())},ah.apply(n,arguments)},t.scaleSequentialPow=wv,t.scaleSequentialSqrt=function(){return wv.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Dh(mv());return n.copy=function(){return xv(n,t()).constant(n.constant())},ah.apply(n,arguments)},t.scaleSequentialQuantile=function t(){var e=[],r=vh;function o(t){if(!isNaN(t=+t))return r((i(e,t)-1)/(e.length-1))}return o.domain=function(t){if(!arguments.length)return e.slice();e=[];for(var r,i=0,a=t.length;i1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return Yg.h=360*t-100,Yg.s=1.5-1.5*n,Yg.l=.8-.9*n,Yg+""},t.interpolateWarm=Ug,t.interpolateCool=Og,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,Bg.r=255*(n=Math.sin(t))*n,Bg.g=255*(n=Math.sin(t+Fg))*n,Bg.b=255*(n=Math.sin(t+Ig))*n,Bg+""},t.interpolateViridis=jg,t.interpolateMagma=Xg,t.interpolateInferno=Gg,t.interpolatePlasma=Vg,t.create=function(t){return zt(W(t).call(document.documentElement))},t.creator=W,t.local=qt,t.matcher=tt,t.mouse=Ot,t.namespace=$,t.namespaces=V,t.clientPoint=Ut,t.select=zt,t.selectAll=function(t){return"string"==typeof t?new Ct([document.querySelectorAll(t)],[document.documentElement]):new Ct([null==t?[]:t],Et)},t.selection=Pt,t.selector=Q,t.selectorAll=K,t.style=ct,t.touch=Yt,t.touches=function(t,n){null==n&&(n=Lt().touches);for(var e=0,r=n?n.length:0,i=new Array(r);ed;if(u||(u=c=ji()),hey)if(v>oy-ey)u.moveTo(h*Qg(d),h*ty(d)),u.arc(0,0,h,d,p,!g),l>ey&&(u.moveTo(l*Qg(p),l*ty(p)),u.arc(0,0,l,p,d,g));else{var y,_,b=d,m=p,x=d,w=p,M=v,N=v,A=a.apply(this,arguments)/2,k=A>ey&&(r?+r.apply(this,arguments):ny(l*l+h*h)),S=Kg(Wg(h-l)/2,+e.apply(this,arguments)),T=S,E=S;if(k>ey){var C=ay(k/l*ty(A)),P=ay(k/h*ty(A));(M-=2*C)>ey?(x+=C*=g?1:-1,w-=C):(M=0,x=w=(d+p)/2),(N-=2*P)>ey?(b+=P*=g?1:-1,m-=P):(N=0,b=m=(d+p)/2)}var z=h*Qg(b),R=h*ty(b),q=l*Qg(w),D=l*ty(w);if(S>ey){var L,U=h*Qg(m),O=h*ty(m),Y=l*Qg(x),B=l*ty(x);if(v<=oy-ey&&(L=function(t,n,e,r,i,o,a,u){var c=e-t,f=r-n,s=a-i,l=u-o,h=l*c-s*f;if(!(h*h1?0:s<-1?ry:Math.acos(s))/2),G=ny(L[0]*L[0]+L[1]*L[1]);T=Kg(S,(l-G)/(X-1)),E=Kg(S,(h-G)/(X+1))}}N>ey?E>ey?(y=hy(Y,B,z,R,h,E,g),_=hy(U,O,q,D,h,E,g),u.moveTo(y.cx+y.x01,y.cy+y.y01),Eey&&M>ey?T>ey?(y=hy(q,D,U,O,l,-T,g),_=hy(z,R,Y,B,l,-T,g),u.lineTo(y.cx+y.x01,y.cy+y.y01),T0&&(d+=l);for(null!=n?p.sort(function(t,e){return n(v[t],v[e])}):null!=e&&p.sort(function(t,n){return e(a[t],a[n])}),u=0,f=d?(y-h*b)/d:0;u0?l*f:0)+b,v[c]={data:a[c],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:$g(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:$g(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:$g(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:$g(+t),a):o},a},t.areaRadial=ky,t.radialArea=ky,t.lineRadial=Ay,t.radialLine=Ay,t.pointRadial=Sy,t.linkHorizontal=function(){return Py(zy)},t.linkVertical=function(){return Py(Ry)},t.linkRadial=function(){var t=Py(qy);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.symbol=function(){var t=$g(Dy),n=$g(64),e=null;function r(){var r;if(e||(e=r=ji()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),r)return e=null,r+""||null}return r.type=function(n){return arguments.length?(t="function"==typeof n?n:$g(n),r):t},r.size=function(t){return arguments.length?(n="function"==typeof t?t:$g(+t),r):n},r.context=function(t){return arguments.length?(e=null==t?null:t,r):e},r},t.symbols=Qy,t.symbolCircle=Dy,t.symbolCross=Ly,t.symbolDiamond=Yy,t.symbolSquare=jy,t.symbolStar=Hy,t.symbolTriangle=Gy,t.symbolWye=Zy,t.curveBasisClosed=function(t){return new n_(t)},t.curveBasisOpen=function(t){return new e_(t)},t.curveBasis=function(t){return new t_(t)},t.curveBundle=i_,t.curveCardinalClosed=f_,t.curveCardinalOpen=l_,t.curveCardinal=u_,t.curveCatmullRomClosed=g_,t.curveCatmullRomOpen=__,t.curveCatmullRom=p_,t.curveLinearClosed=function(t){return new b_(t)},t.curveLinear=py,t.curveMonotoneX=function(t){return new N_(t)},t.curveMonotoneY=function(t){return new A_(t)},t.curveNatural=function(t){return new S_(t)},t.curveStep=function(t){return new E_(t,.5)},t.curveStepAfter=function(t){return new E_(t,1)},t.curveStepBefore=function(t){return new E_(t,0)},t.stack=function(){var t=$g([]),n=P_,e=C_,r=z_;function i(i){var o,a,u=t.apply(this,arguments),c=i.length,f=u.length,s=new Array(f);for(o=0;o0){for(var e,r,i,o=0,a=t[0].length;o1)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):r[0]=o},t.stackOffsetNone=C_,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;adr&&e.name===n)return new Er([[t]],fi,n,+r);return null},t.interrupt=Mr,t.voronoi=function(){var t=O_,n=Y_,e=null;function r(r){return new bb(r.map(function(e,i){var o=[Math.round(t(e,i,r)/gb)*gb,Math.round(n(e,i,r)/gb)*gb];return o.index=i,o.data=e,o}),e)}return r.polygons=function(t){return r(t).polygons()},r.links=function(t){return r(t).links()},r.triangles=function(t){return r(t).triangles()},r.x=function(n){return arguments.length?(t="function"==typeof n?n:U_(+n),r):t},r.y=function(t){return arguments.length?(n="function"==typeof t?t:U_(+t),r):n},r.extent=function(t){return arguments.length?(e=null==t?null:[[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]],r):e&&[[e[0][0],e[0][1]],[e[1][0],e[1][1]]]},r.size=function(t){return arguments.length?(e=null==t?null:[[0,0],[+t[0],+t[1]]],r):e&&[e[1][0]-e[0][0],e[1][1]-e[0][1]]},r},t.zoom=function(){var n,e,r=Sb,i=Tb,o=zb,a=Cb,u=Pb,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=qe,h=[],d=I("start","zoom","end"),p=500,v=150,g=0;function y(t){t.property("__zoom",Eb).on("wheel.zoom",N).on("mousedown.zoom",A).on("dblclick.zoom",k).filter(u).on("touchstart.zoom",S).on("touchmove.zoom",T).on("touchend.zoom touchcancel.zoom",E).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new wb(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new wb(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e){t.on("start.zoom",function(){w(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){w(this,arguments).end()}).tween("zoom",function(){var t=arguments,r=w(this,t),o=i.apply(this,t),a=e||m(o),u=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),c=this.__zoom,f="function"==typeof n?n.apply(this,t):n,s=l(c.invert(a).concat(u/c.k),f.invert(a).concat(u/f.k));return function(t){if(1===t)t=f;else{var n=s(t),e=u/n[2];t=new wb(e,a[0]-n[0]*e,a[1]-n[1]*e)}r.zoom(null,t)}})}function w(t,n){for(var e,r=0,i=h.length;rg}n.zoom("mouse",o(b(n.that.__zoom,n.mouse[0]=Ot(n.that),n.mouse[1]),n.extent,f))},!0).on("mouseup.zoom",function(){i.on("mousemove.zoom mouseup.zoom",null),Ht(t.event.view,n.moved),kb(),n.end()},!0),a=Ot(this),u=t.event.clientX,c=t.event.clientY;It(t.event.view),Ab(),n.mouse=[a,this.__zoom.invert(a)],Mr(this),n.start()}}function k(){if(r.apply(this,arguments)){var n=this.__zoom,e=Ot(this),a=n.invert(e),u=n.k*(t.event.shiftKey?.5:2),c=o(b(_(n,u),e,a),i.apply(this,arguments),f);kb(),s>0?zt(this).transition().duration(s).call(x,c,e):zt(this).call(y.transform,c)}}function S(){if(r.apply(this,arguments)){var e,i,o,a,u=w(this,arguments),c=t.event.changedTouches,f=c.length;for(Ab(),i=0;ia==f>-a?(h=a,a=i[++c]):(h=f,f=e[++u]);let _=0;if(ca==f>-a?(r=a+h,l=h-(r-a),a=i[++c]):(r=f+h,l=h-(r-f),f=e[++u]),h=r,0!==l&&(n[_++]=l);ca==f>-a?(r=h+a,o=r-h,l=h-(r-o)+(a-o),a=i[++c]):(r=h+f,o=r-h,l=h-(r-o)+(f-o),f=e[++u]),h=r,0!==l&&(n[_++]=l);for(;c0!=d>0)return g;const y=Math.abs(_+d);return Math.abs(g)>=33306690738754716e-32*y?g:-function(s,o,a,f,c,u,_){let d,g,y,w,b,A,k,M,p,x,S,T,z,U,m,K,L,v;const F=s-c,P=a-c,E=o-u,H=f-u;U=F*H,A=t*F,k=A-(A-F),M=F-k,A=t*H,p=A-(A-H),x=H-p,m=M*x-(U-k*p-M*p-k*x),K=E*P,A=t*E,k=A-(A-E),M=E-k,A=t*P,p=A-(A-P),x=P-p,L=M*x-(K-k*p-M*p-k*x),S=m-L,b=m-S,e[0]=m-(S+b)+(b-L),T=U+S,b=T-U,z=U-(T-b)+(S-b),S=z-K,b=z-S,e[1]=z-(S+b)+(b-K),v=T+S,b=v-T,e[2]=T-(v-b)+(S-b),e[3]=v;let I=function(t,i){let s=i[0];for(let e=1;e=N||-I>=N)return I;if(b=s-F,d=s-(F+b)+(b-c),b=a-P,y=a-(P+b)+(b-c),b=o-E,g=o-(E+b)+(b-u),b=f-H,w=f-(H+b)+(b-u),0===d&&0===g&&0===y&&0===w)return I;if(N=11093356479670487e-47*_+33306690738754706e-32*Math.abs(I),I+=F*w+H*d-(E*y+P*g),I>=N||-I>=N)return I;U=d*H,A=t*d,k=A-(A-d),M=d-k,A=t*H,p=A-(A-H),x=H-p,m=M*x-(U-k*p-M*p-k*x),K=g*P,A=t*g,k=A-(A-g),M=g-k,A=t*P,p=A-(A-P),x=P-p,L=M*x-(K-k*p-M*p-k*x),S=m-L,b=m-S,l[0]=m-(S+b)+(b-L),T=U+S,b=T-U,z=U-(T-b)+(S-b),S=z-K,b=z-S,l[1]=z-(S+b)+(b-K),v=T+S,b=v-T,l[2]=T-(v-b)+(S-b),l[3]=v;const j=i(4,e,4,l,n);U=F*w,A=t*F,k=A-(A-F),M=F-k,A=t*w,p=A-(A-w),x=w-p,m=M*x-(U-k*p-M*p-k*x),K=E*y,A=t*E,k=A-(A-E),M=E-k,A=t*y,p=A-(A-y),x=y-p,L=M*x-(K-k*p-M*p-k*x),S=m-L,b=m-S,l[0]=m-(S+b)+(b-L),T=U+S,b=T-U,z=U-(T-b)+(S-b),S=z-K,b=z-S,l[1]=z-(S+b)+(b-K),v=T+S,b=v-T,l[2]=T-(v-b)+(S-b),l[3]=v;const q=i(j,n,4,l,h);U=d*w,A=t*d,k=A-(A-d),M=d-k,A=t*w,p=A-(A-w),x=w-p,m=M*x-(U-k*p-M*p-k*x),K=g*y,A=t*g,k=A-(A-g),M=g-k,A=t*y,p=A-(A-y),x=y-p,L=M*x-(K-k*p-M*p-k*x),S=m-L,b=m-S,l[0]=m-(S+b)+(b-L),T=U+S,b=T-U,z=U-(T-b)+(S-b),S=z-K,b=z-S,l[1]=z-(S+b)+(b-K),v=T+S,b=v-T,l[2]=T-(v-b)+(S-b),l[3]=v;const D=i(q,h,4,l,r);return r[D-1]}(s,o,a,f,c,u,y)}const a=Math.pow(2,-52),f=new Uint32Array(512);class c{static from(t,i=w,s=b){const e=t.length,n=new Float64Array(2*e);for(let h=0;h>1;if(i>0&&"number"!=typeof t[0])throw new Error("Expected coords to contain numbers.");this.coords=t;const s=Math.max(2*i-5,0);this._triangles=new Uint32Array(3*s),this._halfedges=new Int32Array(3*s),this._hashSize=Math.ceil(Math.sqrt(i)),this._hullPrev=new Uint32Array(i),this._hullNext=new Uint32Array(i),this._hullTri=new Uint32Array(i),this._hullHash=new Int32Array(this._hashSize).fill(-1),this._ids=new Uint32Array(i),this._dists=new Float64Array(i),this.update()}update(){const{coords:t,_hullPrev:i,_hullNext:s,_hullTri:e,_hullHash:n}=this,h=t.length>>1;let r=1/0,l=1/0,f=-1/0,c=-1/0;for(let i=0;if&&(f=s),e>c&&(c=e),this._ids[i]=i}const _=(r+f)/2,y=(l+c)/2;let w,b,A,k=1/0;for(let i=0;i0&&(b=i,k=s)}let x=t[2*b],S=t[2*b+1],T=1/0;for(let i=0;ie&&(i[s++]=n,e=this._dists[n])}return this.hull=i.subarray(0,s),this.triangles=new Uint32Array(0),void(this.halfedges=new Uint32Array(0))}if(o(M,p,x,S,z,U)<0){const t=b,i=x,s=S;b=A,x=z,S=U,A=t,z=i,U=s}const m=function(t,i,s,e,n,h){const r=s-t,l=e-i,o=n-t,a=h-i,f=r*r+l*l,c=o*o+a*a,u=.5/(r*a-l*o);return{x:t+(a*f-l*c)*u,y:i+(r*c-o*f)*u}}(M,p,x,S,z,U);this._cx=m.x,this._cy=m.y;for(let i=0;i0&&Math.abs(c-h)<=a&&Math.abs(u-r)<=a)continue;if(h=c,r=u,f===w||f===b||f===A)continue;let _=0;for(let t=0,i=this._hashKey(c,u);t=0;)if(g=d,g===_){g=-1;break}if(-1===g)continue;let y=this._addTriangle(g,f,s[g],-1,-1,e[g]);e[f]=this._legalize(y+2),e[g]=y,K++;let k=s[g];for(;d=s[k],o(c,u,t[2*k],t[2*k+1],t[2*d],t[2*d+1])<0;)y=this._addTriangle(k,f,d,e[f],-1,e[k]),e[f]=this._legalize(y+2),s[k]=k,K--,k=d;if(g===_)for(;d=i[g],o(c,u,t[2*d],t[2*d+1],t[2*g],t[2*g+1])<0;)y=this._addTriangle(d,f,g,-1,e[g],e[d]),this._legalize(y+2),e[d]=y,s[g]=g,K--,g=d;this._hullStart=i[f]=g,s[g]=i[k]=f,s[f]=k,n[this._hashKey(c,u)]=f,n[this._hashKey(t[2*g],t[2*g+1])]=g}this.hull=new Uint32Array(K);for(let t=0,i=this._hullStart;t0?3-s:1+s)/4}(t-this._cx,i-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{_triangles:i,_halfedges:s,coords:e}=this;let n=0,h=0;for(;;){const r=s[t],l=t-t%3;if(h=l+(t+2)%3,-1===r){if(0===n)break;t=f[--n];continue}const o=r-r%3,a=l+(t+1)%3,c=o+(r+2)%3,u=i[h],d=i[t],g=i[a],y=i[c];if(_(e[2*u],e[2*u+1],e[2*d],e[2*d+1],e[2*g],e[2*g+1],e[2*y],e[2*y+1])){i[t]=y,i[r]=u;const e=s[c];if(-1===e){let i=this._hullStart;do{if(this._hullTri[i]===c){this._hullTri[i]=t;break}i=this._hullPrev[i]}while(i!==this._hullStart)}this._link(t,e),this._link(r,s[h]),this._link(h,c);const l=o+(r+1)%3;n=s&&i[t[r]]>h;)t[r+1]=t[r--];t[r+1]=e}else{let n=s+1,h=e;y(t,s+e>>1,n),i[t[s]]>i[t[e]]&&y(t,s,e),i[t[n]]>i[t[e]]&&y(t,n,e),i[t[s]]>i[t[n]]&&y(t,s,n);const r=t[n],l=i[r];for(;;){do{n++}while(i[t[n]]l);if(h=h-s?(g(t,i,n,e),g(t,i,s,h-1)):(g(t,i,s,h-1),g(t,i,n,e))}}function y(t,i,s){const e=t[i];t[i]=t[s],t[s]=e}function w(t){return t[0]}function b(t){return t[1]}return c}));
diff --git a/procedural/src/viewer/libs/dropbox-sdk.min.js b/procedural/src/viewer/libs/dropbox-sdk.min.js
new file mode 100644
index 00000000..c5e32fc2
--- /dev/null
+++ b/procedural/src/viewer/libs/dropbox-sdk.min.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Dropbox={})}(this,(function(e){"use strict";function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){for(var r=0;t.length>r;r++){var s=t[r];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function s(e,t,s){return t&&r(e.prototype,t),s&&r(e,s),e}function i(e){return(i=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function n(e,t){return(n=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function u(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}function a(e,t,r){return(a=u()?Reflect.construct:function(e,t,r){var s=[null];s.push.apply(s,t);var i=new(Function.bind.apply(e,s));return r&&n(i,r.prototype),i}).apply(null,arguments)}function o(e){var t="function"==typeof Map?new Map:void 0;return(o=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return a(e,arguments,i(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),n(r,e)})(e)}function c(e,t){return!t||"object"!=typeof t&&"function"!=typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}var p="app",l="user",f="team",h="dropboxapi.com",m="dropbox.com",d={api:"api",notify:"bolt",content:"api-content"},_={};function q(e){var t="000".concat(e.charCodeAt(0).toString(16)).slice(-4);return"\\u".concat(t)}_.accountSetProfilePhoto=function(e){return this.request("account/set_profile_photo",e,"user","api","rpc")},_.authTokenFromOauth1=function(e){return this.request("auth/token/from_oauth1",e,"app","api","rpc")},_.authTokenRevoke=function(){return this.request("auth/token/revoke",null,"user","api","rpc")},_.checkApp=function(e){return this.request("check/app",e,"app","api","rpc")},_.checkUser=function(e){return this.request("check/user",e,"user","api","rpc")},_.contactsDeleteManualContacts=function(){return this.request("contacts/delete_manual_contacts",null,"user","api","rpc")},_.contactsDeleteManualContactsBatch=function(e){return this.request("contacts/delete_manual_contacts_batch",e,"user","api","rpc")},_.filePropertiesPropertiesAdd=function(e){return this.request("file_properties/properties/add",e,"user","api","rpc")},_.filePropertiesPropertiesOverwrite=function(e){return this.request("file_properties/properties/overwrite",e,"user","api","rpc")},_.filePropertiesPropertiesRemove=function(e){return this.request("file_properties/properties/remove",e,"user","api","rpc")},_.filePropertiesPropertiesSearch=function(e){return this.request("file_properties/properties/search",e,"user","api","rpc")},_.filePropertiesPropertiesSearchContinue=function(e){return this.request("file_properties/properties/search/continue",e,"user","api","rpc")},_.filePropertiesPropertiesUpdate=function(e){return this.request("file_properties/properties/update",e,"user","api","rpc")},_.filePropertiesTemplatesAddForTeam=function(e){return this.request("file_properties/templates/add_for_team",e,"team","api","rpc")},_.filePropertiesTemplatesAddForUser=function(e){return this.request("file_properties/templates/add_for_user",e,"user","api","rpc")},_.filePropertiesTemplatesGetForTeam=function(e){return this.request("file_properties/templates/get_for_team",e,"team","api","rpc")},_.filePropertiesTemplatesGetForUser=function(e){return this.request("file_properties/templates/get_for_user",e,"user","api","rpc")},_.filePropertiesTemplatesListForTeam=function(){return this.request("file_properties/templates/list_for_team",null,"team","api","rpc")},_.filePropertiesTemplatesListForUser=function(){return this.request("file_properties/templates/list_for_user",null,"user","api","rpc")},_.filePropertiesTemplatesRemoveForTeam=function(e){return this.request("file_properties/templates/remove_for_team",e,"team","api","rpc")},_.filePropertiesTemplatesRemoveForUser=function(e){return this.request("file_properties/templates/remove_for_user",e,"user","api","rpc")},_.filePropertiesTemplatesUpdateForTeam=function(e){return this.request("file_properties/templates/update_for_team",e,"team","api","rpc")},_.filePropertiesTemplatesUpdateForUser=function(e){return this.request("file_properties/templates/update_for_user",e,"user","api","rpc")},_.fileRequestsCount=function(){return this.request("file_requests/count",null,"user","api","rpc")},_.fileRequestsCreate=function(e){return this.request("file_requests/create",e,"user","api","rpc")},_.fileRequestsDelete=function(e){return this.request("file_requests/delete",e,"user","api","rpc")},_.fileRequestsDeleteAllClosed=function(){return this.request("file_requests/delete_all_closed",null,"user","api","rpc")},_.fileRequestsGet=function(e){return this.request("file_requests/get",e,"user","api","rpc")},_.fileRequestsListV2=function(e){return this.request("file_requests/list_v2",e,"user","api","rpc")},_.fileRequestsList=function(){return this.request("file_requests/list",null,"user","api","rpc")},_.fileRequestsListContinue=function(e){return this.request("file_requests/list/continue",e,"user","api","rpc")},_.fileRequestsUpdate=function(e){return this.request("file_requests/update",e,"user","api","rpc")},_.filesAlphaGetMetadata=function(e){return this.request("files/alpha/get_metadata",e,"user","api","rpc")},_.filesAlphaUpload=function(e){return this.request("files/alpha/upload",e,"user","content","upload")},_.filesCopyV2=function(e){return this.request("files/copy_v2",e,"user","api","rpc")},_.filesCopy=function(e){return this.request("files/copy",e,"user","api","rpc")},_.filesCopyBatchV2=function(e){return this.request("files/copy_batch_v2",e,"user","api","rpc")},_.filesCopyBatch=function(e){return this.request("files/copy_batch",e,"user","api","rpc")},_.filesCopyBatchCheckV2=function(e){return this.request("files/copy_batch/check_v2",e,"user","api","rpc")},_.filesCopyBatchCheck=function(e){return this.request("files/copy_batch/check",e,"user","api","rpc")},_.filesCopyReferenceGet=function(e){return this.request("files/copy_reference/get",e,"user","api","rpc")},_.filesCopyReferenceSave=function(e){return this.request("files/copy_reference/save",e,"user","api","rpc")},_.filesCreateFolderV2=function(e){return this.request("files/create_folder_v2",e,"user","api","rpc")},_.filesCreateFolder=function(e){return this.request("files/create_folder",e,"user","api","rpc")},_.filesCreateFolderBatch=function(e){return this.request("files/create_folder_batch",e,"user","api","rpc")},_.filesCreateFolderBatchCheck=function(e){return this.request("files/create_folder_batch/check",e,"user","api","rpc")},_.filesDeleteV2=function(e){return this.request("files/delete_v2",e,"user","api","rpc")},_.filesDelete=function(e){return this.request("files/delete",e,"user","api","rpc")},_.filesDeleteBatch=function(e){return this.request("files/delete_batch",e,"user","api","rpc")},_.filesDeleteBatchCheck=function(e){return this.request("files/delete_batch/check",e,"user","api","rpc")},_.filesDownload=function(e){return this.request("files/download",e,"user","content","download")},_.filesDownloadZip=function(e){return this.request("files/download_zip",e,"user","content","download")},_.filesExport=function(e){return this.request("files/export",e,"user","content","download")},_.filesGetFileLockBatch=function(e){return this.request("files/get_file_lock_batch",e,"user","api","rpc")},_.filesGetMetadata=function(e){return this.request("files/get_metadata",e,"user","api","rpc")},_.filesGetPreview=function(e){return this.request("files/get_preview",e,"user","content","download")},_.filesGetTemporaryLink=function(e){return this.request("files/get_temporary_link",e,"user","api","rpc")},_.filesGetTemporaryUploadLink=function(e){return this.request("files/get_temporary_upload_link",e,"user","api","rpc")},_.filesGetThumbnail=function(e){return this.request("files/get_thumbnail",e,"user","content","download")},_.filesGetThumbnailV2=function(e){return this.request("files/get_thumbnail_v2",e,"app, user","content","download")},_.filesGetThumbnailBatch=function(e){return this.request("files/get_thumbnail_batch",e,"user","content","rpc")},_.filesListFolder=function(e){return this.request("files/list_folder",e,"user","api","rpc")},_.filesListFolderContinue=function(e){return this.request("files/list_folder/continue",e,"user","api","rpc")},_.filesListFolderGetLatestCursor=function(e){return this.request("files/list_folder/get_latest_cursor",e,"user","api","rpc")},_.filesListFolderLongpoll=function(e){return this.request("files/list_folder/longpoll",e,"noauth","notify","rpc")},_.filesListRevisions=function(e){return this.request("files/list_revisions",e,"user","api","rpc")},_.filesLockFileBatch=function(e){return this.request("files/lock_file_batch",e,"user","api","rpc")},_.filesMoveV2=function(e){return this.request("files/move_v2",e,"user","api","rpc")},_.filesMove=function(e){return this.request("files/move",e,"user","api","rpc")},_.filesMoveBatchV2=function(e){return this.request("files/move_batch_v2",e,"user","api","rpc")},_.filesMoveBatch=function(e){return this.request("files/move_batch",e,"user","api","rpc")},_.filesMoveBatchCheckV2=function(e){return this.request("files/move_batch/check_v2",e,"user","api","rpc")},_.filesMoveBatchCheck=function(e){return this.request("files/move_batch/check",e,"user","api","rpc")},_.filesPaperCreate=function(e){return this.request("files/paper/create",e,"user","api","upload")},_.filesPaperUpdate=function(e){return this.request("files/paper/update",e,"user","api","upload")},_.filesPermanentlyDelete=function(e){return this.request("files/permanently_delete",e,"user","api","rpc")},_.filesPropertiesAdd=function(e){return this.request("files/properties/add",e,"user","api","rpc")},_.filesPropertiesOverwrite=function(e){return this.request("files/properties/overwrite",e,"user","api","rpc")},_.filesPropertiesRemove=function(e){return this.request("files/properties/remove",e,"user","api","rpc")},_.filesPropertiesTemplateGet=function(e){return this.request("files/properties/template/get",e,"user","api","rpc")},_.filesPropertiesTemplateList=function(){return this.request("files/properties/template/list",null,"user","api","rpc")},_.filesPropertiesUpdate=function(e){return this.request("files/properties/update",e,"user","api","rpc")},_.filesRestore=function(e){return this.request("files/restore",e,"user","api","rpc")},_.filesSaveUrl=function(e){return this.request("files/save_url",e,"user","api","rpc")},_.filesSaveUrlCheckJobStatus=function(e){return this.request("files/save_url/check_job_status",e,"user","api","rpc")},_.filesSearch=function(e){return this.request("files/search",e,"user","api","rpc")},_.filesSearchV2=function(e){return this.request("files/search_v2",e,"user","api","rpc")},_.filesSearchContinueV2=function(e){return this.request("files/search/continue_v2",e,"user","api","rpc")},_.filesUnlockFileBatch=function(e){return this.request("files/unlock_file_batch",e,"user","api","rpc")},_.filesUpload=function(e){return this.request("files/upload",e,"user","content","upload")},_.filesUploadSessionAppendV2=function(e){return this.request("files/upload_session/append_v2",e,"user","content","upload")},_.filesUploadSessionAppend=function(e){return this.request("files/upload_session/append",e,"user","content","upload")},_.filesUploadSessionFinish=function(e){return this.request("files/upload_session/finish",e,"user","content","upload")},_.filesUploadSessionFinishBatch=function(e){return this.request("files/upload_session/finish_batch",e,"user","api","rpc")},_.filesUploadSessionFinishBatchCheck=function(e){return this.request("files/upload_session/finish_batch/check",e,"user","api","rpc")},_.filesUploadSessionStart=function(e){return this.request("files/upload_session/start",e,"user","content","upload")},_.paperDocsArchive=function(e){return this.request("paper/docs/archive",e,"user","api","rpc")},_.paperDocsCreate=function(e){return this.request("paper/docs/create",e,"user","api","upload")},_.paperDocsDownload=function(e){return this.request("paper/docs/download",e,"user","api","download")},_.paperDocsFolderUsersList=function(e){return this.request("paper/docs/folder_users/list",e,"user","api","rpc")},_.paperDocsFolderUsersListContinue=function(e){return this.request("paper/docs/folder_users/list/continue",e,"user","api","rpc")},_.paperDocsGetFolderInfo=function(e){return this.request("paper/docs/get_folder_info",e,"user","api","rpc")},_.paperDocsList=function(e){return this.request("paper/docs/list",e,"user","api","rpc")},_.paperDocsListContinue=function(e){return this.request("paper/docs/list/continue",e,"user","api","rpc")},_.paperDocsPermanentlyDelete=function(e){return this.request("paper/docs/permanently_delete",e,"user","api","rpc")},_.paperDocsSharingPolicyGet=function(e){return this.request("paper/docs/sharing_policy/get",e,"user","api","rpc")},_.paperDocsSharingPolicySet=function(e){return this.request("paper/docs/sharing_policy/set",e,"user","api","rpc")},_.paperDocsUpdate=function(e){return this.request("paper/docs/update",e,"user","api","upload")},_.paperDocsUsersAdd=function(e){return this.request("paper/docs/users/add",e,"user","api","rpc")},_.paperDocsUsersList=function(e){return this.request("paper/docs/users/list",e,"user","api","rpc")},_.paperDocsUsersListContinue=function(e){return this.request("paper/docs/users/list/continue",e,"user","api","rpc")},_.paperDocsUsersRemove=function(e){return this.request("paper/docs/users/remove",e,"user","api","rpc")},_.paperFoldersCreate=function(e){return this.request("paper/folders/create",e,"user","api","rpc")},_.sharingAddFileMember=function(e){return this.request("sharing/add_file_member",e,"user","api","rpc")},_.sharingAddFolderMember=function(e){return this.request("sharing/add_folder_member",e,"user","api","rpc")},_.sharingCheckJobStatus=function(e){return this.request("sharing/check_job_status",e,"user","api","rpc")},_.sharingCheckRemoveMemberJobStatus=function(e){return this.request("sharing/check_remove_member_job_status",e,"user","api","rpc")},_.sharingCheckShareJobStatus=function(e){return this.request("sharing/check_share_job_status",e,"user","api","rpc")},_.sharingCreateSharedLink=function(e){return this.request("sharing/create_shared_link",e,"user","api","rpc")},_.sharingCreateSharedLinkWithSettings=function(e){return this.request("sharing/create_shared_link_with_settings",e,"user","api","rpc")},_.sharingGetFileMetadata=function(e){return this.request("sharing/get_file_metadata",e,"user","api","rpc")},_.sharingGetFileMetadataBatch=function(e){return this.request("sharing/get_file_metadata/batch",e,"user","api","rpc")},_.sharingGetFolderMetadata=function(e){return this.request("sharing/get_folder_metadata",e,"user","api","rpc")},_.sharingGetSharedLinkFile=function(e){return this.request("sharing/get_shared_link_file",e,"user","content","download")},_.sharingGetSharedLinkMetadata=function(e){return this.request("sharing/get_shared_link_metadata",e,"user","api","rpc")},_.sharingGetSharedLinks=function(e){return this.request("sharing/get_shared_links",e,"user","api","rpc")},_.sharingListFileMembers=function(e){return this.request("sharing/list_file_members",e,"user","api","rpc")},_.sharingListFileMembersBatch=function(e){return this.request("sharing/list_file_members/batch",e,"user","api","rpc")},_.sharingListFileMembersContinue=function(e){return this.request("sharing/list_file_members/continue",e,"user","api","rpc")},_.sharingListFolderMembers=function(e){return this.request("sharing/list_folder_members",e,"user","api","rpc")},_.sharingListFolderMembersContinue=function(e){return this.request("sharing/list_folder_members/continue",e,"user","api","rpc")},_.sharingListFolders=function(e){return this.request("sharing/list_folders",e,"user","api","rpc")},_.sharingListFoldersContinue=function(e){return this.request("sharing/list_folders/continue",e,"user","api","rpc")},_.sharingListMountableFolders=function(e){return this.request("sharing/list_mountable_folders",e,"user","api","rpc")},_.sharingListMountableFoldersContinue=function(e){return this.request("sharing/list_mountable_folders/continue",e,"user","api","rpc")},_.sharingListReceivedFiles=function(e){return this.request("sharing/list_received_files",e,"user","api","rpc")},_.sharingListReceivedFilesContinue=function(e){return this.request("sharing/list_received_files/continue",e,"user","api","rpc")},_.sharingListSharedLinks=function(e){return this.request("sharing/list_shared_links",e,"user","api","rpc")},_.sharingModifySharedLinkSettings=function(e){return this.request("sharing/modify_shared_link_settings",e,"user","api","rpc")},_.sharingMountFolder=function(e){return this.request("sharing/mount_folder",e,"user","api","rpc")},_.sharingRelinquishFileMembership=function(e){return this.request("sharing/relinquish_file_membership",e,"user","api","rpc")},_.sharingRelinquishFolderMembership=function(e){return this.request("sharing/relinquish_folder_membership",e,"user","api","rpc")},_.sharingRemoveFileMember=function(e){return this.request("sharing/remove_file_member",e,"user","api","rpc")},_.sharingRemoveFileMember2=function(e){return this.request("sharing/remove_file_member_2",e,"user","api","rpc")},_.sharingRemoveFolderMember=function(e){return this.request("sharing/remove_folder_member",e,"user","api","rpc")},_.sharingRevokeSharedLink=function(e){return this.request("sharing/revoke_shared_link",e,"user","api","rpc")},_.sharingSetAccessInheritance=function(e){return this.request("sharing/set_access_inheritance",e,"user","api","rpc")},_.sharingShareFolder=function(e){return this.request("sharing/share_folder",e,"user","api","rpc")},_.sharingTransferFolder=function(e){return this.request("sharing/transfer_folder",e,"user","api","rpc")},_.sharingUnmountFolder=function(e){return this.request("sharing/unmount_folder",e,"user","api","rpc")},_.sharingUnshareFile=function(e){return this.request("sharing/unshare_file",e,"user","api","rpc")},_.sharingUnshareFolder=function(e){return this.request("sharing/unshare_folder",e,"user","api","rpc")},_.sharingUpdateFileMember=function(e){return this.request("sharing/update_file_member",e,"user","api","rpc")},_.sharingUpdateFolderMember=function(e){return this.request("sharing/update_folder_member",e,"user","api","rpc")},_.sharingUpdateFolderPolicy=function(e){return this.request("sharing/update_folder_policy",e,"user","api","rpc")},_.teamDevicesListMemberDevices=function(e){return this.request("team/devices/list_member_devices",e,"team","api","rpc")},_.teamDevicesListMembersDevices=function(e){return this.request("team/devices/list_members_devices",e,"team","api","rpc")},_.teamDevicesListTeamDevices=function(e){return this.request("team/devices/list_team_devices",e,"team","api","rpc")},_.teamDevicesRevokeDeviceSession=function(e){return this.request("team/devices/revoke_device_session",e,"team","api","rpc")},_.teamDevicesRevokeDeviceSessionBatch=function(e){return this.request("team/devices/revoke_device_session_batch",e,"team","api","rpc")},_.teamFeaturesGetValues=function(e){return this.request("team/features/get_values",e,"team","api","rpc")},_.teamGetInfo=function(){return this.request("team/get_info",null,"team","api","rpc")},_.teamGroupsCreate=function(e){return this.request("team/groups/create",e,"team","api","rpc")},_.teamGroupsDelete=function(e){return this.request("team/groups/delete",e,"team","api","rpc")},_.teamGroupsGetInfo=function(e){return this.request("team/groups/get_info",e,"team","api","rpc")},_.teamGroupsJobStatusGet=function(e){return this.request("team/groups/job_status/get",e,"team","api","rpc")},_.teamGroupsList=function(e){return this.request("team/groups/list",e,"team","api","rpc")},_.teamGroupsListContinue=function(e){return this.request("team/groups/list/continue",e,"team","api","rpc")},_.teamGroupsMembersAdd=function(e){return this.request("team/groups/members/add",e,"team","api","rpc")},_.teamGroupsMembersList=function(e){return this.request("team/groups/members/list",e,"team","api","rpc")},_.teamGroupsMembersListContinue=function(e){return this.request("team/groups/members/list/continue",e,"team","api","rpc")},_.teamGroupsMembersRemove=function(e){return this.request("team/groups/members/remove",e,"team","api","rpc")},_.teamGroupsMembersSetAccessType=function(e){return this.request("team/groups/members/set_access_type",e,"team","api","rpc")},_.teamGroupsUpdate=function(e){return this.request("team/groups/update",e,"team","api","rpc")},_.teamLegalHoldsCreatePolicy=function(e){return this.request("team/legal_holds/create_policy",e,"team","api","rpc")},_.teamLegalHoldsGetPolicy=function(e){return this.request("team/legal_holds/get_policy",e,"team","api","rpc")},_.teamLegalHoldsListHeldRevisions=function(e){return this.request("team/legal_holds/list_held_revisions",e,"team","api","rpc")},_.teamLegalHoldsListHeldRevisionsContinue=function(e){return this.request("team/legal_holds/list_held_revisions_continue",e,"team","api","rpc")},_.teamLegalHoldsListPolicies=function(e){return this.request("team/legal_holds/list_policies",e,"team","api","rpc")},_.teamLegalHoldsReleasePolicy=function(e){return this.request("team/legal_holds/release_policy",e,"team","api","rpc")},_.teamLegalHoldsUpdatePolicy=function(e){return this.request("team/legal_holds/update_policy",e,"team","api","rpc")},_.teamLinkedAppsListMemberLinkedApps=function(e){return this.request("team/linked_apps/list_member_linked_apps",e,"team","api","rpc")},_.teamLinkedAppsListMembersLinkedApps=function(e){return this.request("team/linked_apps/list_members_linked_apps",e,"team","api","rpc")},_.teamLinkedAppsListTeamLinkedApps=function(e){return this.request("team/linked_apps/list_team_linked_apps",e,"team","api","rpc")},_.teamLinkedAppsRevokeLinkedApp=function(e){return this.request("team/linked_apps/revoke_linked_app",e,"team","api","rpc")},_.teamLinkedAppsRevokeLinkedAppBatch=function(e){return this.request("team/linked_apps/revoke_linked_app_batch",e,"team","api","rpc")},_.teamMemberSpaceLimitsExcludedUsersAdd=function(e){return this.request("team/member_space_limits/excluded_users/add",e,"team","api","rpc")},_.teamMemberSpaceLimitsExcludedUsersList=function(e){return this.request("team/member_space_limits/excluded_users/list",e,"team","api","rpc")},_.teamMemberSpaceLimitsExcludedUsersListContinue=function(e){return this.request("team/member_space_limits/excluded_users/list/continue",e,"team","api","rpc")},_.teamMemberSpaceLimitsExcludedUsersRemove=function(e){return this.request("team/member_space_limits/excluded_users/remove",e,"team","api","rpc")},_.teamMemberSpaceLimitsGetCustomQuota=function(e){return this.request("team/member_space_limits/get_custom_quota",e,"team","api","rpc")},_.teamMemberSpaceLimitsRemoveCustomQuota=function(e){return this.request("team/member_space_limits/remove_custom_quota",e,"team","api","rpc")},_.teamMemberSpaceLimitsSetCustomQuota=function(e){return this.request("team/member_space_limits/set_custom_quota",e,"team","api","rpc")},_.teamMembersAddV2=function(e){return this.request("team/members/add_v2",e,"team","api","rpc")},_.teamMembersAdd=function(e){return this.request("team/members/add",e,"team","api","rpc")},_.teamMembersAddJobStatusGetV2=function(e){return this.request("team/members/add/job_status/get_v2",e,"team","api","rpc")},_.teamMembersAddJobStatusGet=function(e){return this.request("team/members/add/job_status/get",e,"team","api","rpc")},_.teamMembersDeleteProfilePhotoV2=function(e){return this.request("team/members/delete_profile_photo_v2",e,"team","api","rpc")},_.teamMembersDeleteProfilePhoto=function(e){return this.request("team/members/delete_profile_photo",e,"team","api","rpc")},_.teamMembersGetAvailableTeamMemberRoles=function(){return this.request("team/members/get_available_team_member_roles",null,"team","api","rpc")},_.teamMembersGetInfoV2=function(e){return this.request("team/members/get_info_v2",e,"team","api","rpc")},_.teamMembersGetInfo=function(e){return this.request("team/members/get_info",e,"team","api","rpc")},_.teamMembersListV2=function(e){return this.request("team/members/list_v2",e,"team","api","rpc")},_.teamMembersList=function(e){return this.request("team/members/list",e,"team","api","rpc")},_.teamMembersListContinueV2=function(e){return this.request("team/members/list/continue_v2",e,"team","api","rpc")},_.teamMembersListContinue=function(e){return this.request("team/members/list/continue",e,"team","api","rpc")},_.teamMembersMoveFormerMemberFiles=function(e){return this.request("team/members/move_former_member_files",e,"team","api","rpc")},_.teamMembersMoveFormerMemberFilesJobStatusCheck=function(e){return this.request("team/members/move_former_member_files/job_status/check",e,"team","api","rpc")},_.teamMembersRecover=function(e){return this.request("team/members/recover",e,"team","api","rpc")},_.teamMembersRemove=function(e){return this.request("team/members/remove",e,"team","api","rpc")},_.teamMembersRemoveJobStatusGet=function(e){return this.request("team/members/remove/job_status/get",e,"team","api","rpc")},_.teamMembersSecondaryEmailsAdd=function(e){return this.request("team/members/secondary_emails/add",e,"team","api","rpc")},_.teamMembersSecondaryEmailsDelete=function(e){return this.request("team/members/secondary_emails/delete",e,"team","api","rpc")},_.teamMembersSecondaryEmailsResendVerificationEmails=function(e){return this.request("team/members/secondary_emails/resend_verification_emails",e,"team","api","rpc")},_.teamMembersSendWelcomeEmail=function(e){return this.request("team/members/send_welcome_email",e,"team","api","rpc")},_.teamMembersSetAdminPermissionsV2=function(e){return this.request("team/members/set_admin_permissions_v2",e,"team","api","rpc")},_.teamMembersSetAdminPermissions=function(e){return this.request("team/members/set_admin_permissions",e,"team","api","rpc")},_.teamMembersSetProfileV2=function(e){return this.request("team/members/set_profile_v2",e,"team","api","rpc")},_.teamMembersSetProfile=function(e){return this.request("team/members/set_profile",e,"team","api","rpc")},_.teamMembersSetProfilePhotoV2=function(e){return this.request("team/members/set_profile_photo_v2",e,"team","api","rpc")},_.teamMembersSetProfilePhoto=function(e){return this.request("team/members/set_profile_photo",e,"team","api","rpc")},_.teamMembersSuspend=function(e){return this.request("team/members/suspend",e,"team","api","rpc")},_.teamMembersUnsuspend=function(e){return this.request("team/members/unsuspend",e,"team","api","rpc")},_.teamNamespacesList=function(e){return this.request("team/namespaces/list",e,"team","api","rpc")},_.teamNamespacesListContinue=function(e){return this.request("team/namespaces/list/continue",e,"team","api","rpc")},_.teamPropertiesTemplateAdd=function(e){return this.request("team/properties/template/add",e,"team","api","rpc")},_.teamPropertiesTemplateGet=function(e){return this.request("team/properties/template/get",e,"team","api","rpc")},_.teamPropertiesTemplateList=function(){return this.request("team/properties/template/list",null,"team","api","rpc")},_.teamPropertiesTemplateUpdate=function(e){return this.request("team/properties/template/update",e,"team","api","rpc")},_.teamReportsGetActivity=function(e){return this.request("team/reports/get_activity",e,"team","api","rpc")},_.teamReportsGetDevices=function(e){return this.request("team/reports/get_devices",e,"team","api","rpc")},_.teamReportsGetMembership=function(e){return this.request("team/reports/get_membership",e,"team","api","rpc")},_.teamReportsGetStorage=function(e){return this.request("team/reports/get_storage",e,"team","api","rpc")},_.teamTeamFolderActivate=function(e){return this.request("team/team_folder/activate",e,"team","api","rpc")},_.teamTeamFolderArchive=function(e){return this.request("team/team_folder/archive",e,"team","api","rpc")},_.teamTeamFolderArchiveCheck=function(e){return this.request("team/team_folder/archive/check",e,"team","api","rpc")},_.teamTeamFolderCreate=function(e){return this.request("team/team_folder/create",e,"team","api","rpc")},_.teamTeamFolderGetInfo=function(e){return this.request("team/team_folder/get_info",e,"team","api","rpc")},_.teamTeamFolderList=function(e){return this.request("team/team_folder/list",e,"team","api","rpc")},_.teamTeamFolderListContinue=function(e){return this.request("team/team_folder/list/continue",e,"team","api","rpc")},_.teamTeamFolderPermanentlyDelete=function(e){return this.request("team/team_folder/permanently_delete",e,"team","api","rpc")},_.teamTeamFolderRename=function(e){return this.request("team/team_folder/rename",e,"team","api","rpc")},_.teamTeamFolderUpdateSyncSettings=function(e){return this.request("team/team_folder/update_sync_settings",e,"team","api","rpc")},_.teamTokenGetAuthenticatedAdmin=function(){return this.request("team/token/get_authenticated_admin",null,"team","api","rpc")},_.teamLogGetEvents=function(e){return this.request("team_log/get_events",e,"team","api","rpc")},_.teamLogGetEventsContinue=function(e){return this.request("team_log/get_events/continue",e,"team","api","rpc")},_.usersFeaturesGetValues=function(e){return this.request("users/features/get_values",e,"user","api","rpc")},_.usersGetAccount=function(e){return this.request("users/get_account",e,"user","api","rpc")},_.usersGetAccountBatch=function(e){return this.request("users/get_account_batch",e,"user","api","rpc")},_.usersGetCurrentAccount=function(){return this.request("users/get_current_account",null,"user","api","rpc")},_.usersGetSpaceUsage=function(){return this.request("users/get_space_usage",null,"user","api","rpc")};var g=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:h,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:".";return t!==h&&void 0!==d[e]&&(e=d[e],r="-"),"https://".concat(e).concat(r).concat(t,"/2/")},b=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:m;return e!==m&&(e="meta-".concat(e)),"https://".concat(e,"/oauth2/authorize")},v=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:h,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:".",r="api";return e!==h&&(r=d[r],t="-"),"https://".concat(r).concat(t).concat(e,"/oauth2/token")};function k(e){return JSON.stringify(e).replace(/[\u007f-\uffff]/g,q)}function y(e){return new Date(Date.now()+1e3*e)}function A(){return"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope||"undefined"==typeof module||"undefined"!=typeof window}function C(){return"undefined"!=typeof window}function S(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var T,L,w,M=function(e){!function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&n(e,t)}(o,e);var r,s,a=(r=o,s=u(),function(){var e,t=i(r);if(s){var n=i(this).constructor;e=Reflect.construct(t,arguments,n)}else e=t.apply(this,arguments);return c(this,e)});function o(e,r,s){var i;return t(this,o),(i=a.call(this,"Response failed with a ".concat(e," code"))).name="DropboxResponseError",i.status=e,i.headers=r,i.error=s,i}return o}(o(Error)),P=function e(r,s,i){t(this,e),this.status=r,this.headers=s,this.result=i};function R(e){return e.text().then((function(t){var r;try{r=JSON.parse(t)}catch(e){r=t}throw new M(e.status,e.headers,r)}))}function F(e){return e.ok?e.text().then((function(t){var r;try{r=JSON.parse(t)}catch(e){r=t}return new P(e.status,e.headers,r)})):R(e)}T=C()?window.fetch.bind(window):require("node-fetch"),L=C()?window.crypto||window.msCrypto:require("crypto"),w="undefined"==typeof TextEncoder?require("util").TextEncoder:TextEncoder;var D,G=["legacy","offline","online"],U=["code","token"],x=["none","user","team"],E=function(){function e(r){t(this,e),this.fetch=(r=r||{}).fetch||T,this.accessToken=r.accessToken,this.accessTokenExpiresAt=r.accessTokenExpiresAt,this.refreshToken=r.refreshToken,this.clientId=r.clientId,this.clientSecret=r.clientSecret,this.domain=r.domain,this.domainDelimiter=r.domainDelimiter}return s(e,[{key:"setAccessToken",value:function(e){this.accessToken=e}},{key:"getAccessToken",value:function(){return this.accessToken}},{key:"setClientId",value:function(e){this.clientId=e}},{key:"getClientId",value:function(){return this.clientId}},{key:"setClientSecret",value:function(e){this.clientSecret=e}},{key:"getClientSecret",value:function(){return this.clientSecret}},{key:"getRefreshToken",value:function(){return this.refreshToken}},{key:"setRefreshToken",value:function(e){this.refreshToken=e}},{key:"getAccessTokenExpiresAt",value:function(){return this.accessTokenExpiresAt}},{key:"setAccessTokenExpiresAt",value:function(e){this.accessTokenExpiresAt=e}},{key:"setCodeVerifier",value:function(e){this.codeVerifier=e}},{key:"getCodeVerifier",value:function(){return this.codeVerifier}},{key:"generateCodeChallenge",value:function(){var e,t=this,r=(new w).encode(this.codeVerifier);if(C())return L.subtle.digest("SHA-256",r).then((function(r){var s=btoa(String.fromCharCode.apply(null,new Uint8Array(r)));e=S(s).substr(0,128),t.codeChallenge=e}));var s=L.createHash("sha256").update(r).digest();return e=S(s),this.codeChallenge=e,Promise.resolve()}},{key:"generatePKCECodes",value:function(){var e;if(C()){var t=new Uint8Array(128),r=L.getRandomValues(t);e=S(btoa(r)).substr(0,128)}else{e=S(L.randomBytes(128)).substr(0,128)}return this.codeVerifier=e,this.generateCodeChallenge()}},{key:"getAuthenticationUrl",value:function(e,t){var r,s=this,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"token",n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,u=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"none",o=arguments.length>6&&void 0!==arguments[6]&&arguments[6],c=this.getClientId(),p=b(this.domain);if(!c)throw Error("A client id is required. You can set the client id using .setClientId().");if("code"!==i&&!e)throw Error("A redirect uri is required.");if(!U.includes(i))throw Error("Authorization type must be code or token");if(n&&!G.includes(n))throw Error("Token Access Type must be legacy, offline, or online");if(u&&!(u instanceof Array))throw Error("Scope must be an array of strings");if(!x.includes(a))throw Error("includeGrantedScopes must be none, user, or team");return r="code"===i?"".concat(p,"?response_type=code&client_id=").concat(c):"".concat(p,"?response_type=token&client_id=").concat(c),e&&(r+="&redirect_uri=".concat(e)),t&&(r+="&state=".concat(t)),n&&(r+="&token_access_type=".concat(n)),u&&(r+="&scope=".concat(u.join(" "))),"none"!==a&&(r+="&include_granted_scopes=".concat(a)),o?this.generatePKCECodes().then((function(){return r+="&code_challenge_method=S256",r+="&code_challenge=".concat(s.codeChallenge)})):Promise.resolve(r)}},{key:"getAccessTokenFromCode",value:function(e,t){var r=this.getClientId(),s=this.getClientSecret();if(!r)throw Error("A client id is required. You can set the client id using .setClientId().");var i=v(this.domain,this.domainDelimiter);if(i+="?grant_type=authorization_code",i+="&code=".concat(t),i+="&client_id=".concat(r),s)i+="&client_secret=".concat(s);else{if(!this.codeVerifier)throw Error("You must use PKCE when generating the authorization URL to not include a client secret");i+="&code_verifier=".concat(this.codeVerifier)}e&&(i+="&redirect_uri=".concat(e));return this.fetch(i,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"}}).then((function(e){return F(e)}))}},{key:"checkAndRefreshAccessToken",value:function(){var e=this.getRefreshToken()&&this.getClientId(),t=!this.getAccessTokenExpiresAt()||new Date(Date.now()+3e5)>=this.getAccessTokenExpiresAt(),r=!this.getAccessToken();return(t||r)&&e?this.refreshAccessToken():Promise.resolve()}},{key:"refreshAccessToken",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,r=v(this.domain,this.domainDelimiter),s=this.getClientId(),i=this.getClientSecret();if(!s)throw Error("A client id is required. You can set the client id using .setClientId().");if(t&&!(t instanceof Array))throw Error("Scope must be an array of strings");var n={"Content-Type":"application/json"};r+="?grant_type=refresh_token&refresh_token=".concat(this.getRefreshToken()),r+="&client_id=".concat(s),i&&(r+="&client_secret=".concat(i)),t&&(r+="&scope=".concat(t.join(" ")));var u={method:"POST"};return u.headers=n,this.fetch(r,u).then((function(e){return F(e)})).then((function(t){e.setAccessToken(t.result.access_token),e.setAccessTokenExpiresAt(y(t.result.expires_in))}))}}]),e}();D="undefined"!=typeof window?window.fetch.bind(window):require("node-fetch");var V="undefined"==typeof btoa?function(e){return Buffer.from(e).toString("base64")}:btoa;e.Dropbox=function(){function e(r){t(this,e),this.auth=(r=r||{}).auth?r.auth:new E(r),this.fetch=r.fetch||D,this.selectUser=r.selectUser,this.selectAdmin=r.selectAdmin,this.pathRoot=r.pathRoot,this.domain=r.domain,this.domainDelimiter=r.domainDelimiter,Object.assign(this,_)}return s(e,[{key:"request",value:function(e,t,r,s,i){if(r.split(",").length>1){var n=r.replace(" ","").split(",");n.includes(l)&&this.auth.getAccessToken()?r=l:n.includes(f)&&this.auth.getAccessToken()?r=f:n.includes(p)&&(r=p)}switch(i){case"rpc":return this.rpcRequest(e,t,r,s);case"download":return this.downloadRequest(e,t,r,s);case"upload":return this.uploadRequest(e,t,r,s);default:throw Error("Invalid request style: ".concat(i))}}},{key:"rpcRequest",value:function(e,t,r,s){var i=this;return this.auth.checkAndRefreshAccessToken().then((function(){var e,s={method:"POST",body:t?JSON.stringify(t):null,headers:{}};switch(t&&(s.headers["Content-Type"]="application/json"),r){case p:if(!i.auth.clientId||!i.auth.clientSecret)throw Error("A client id and secret is required for this function");e=V("".concat(i.auth.clientId,":").concat(i.auth.clientSecret)),s.headers.Authorization="Basic ".concat(e);break;case f:case l:s.headers.Authorization="Bearer ".concat(i.auth.getAccessToken());break;case"noauth":break;default:throw Error("Unhandled auth type: ".concat(r))}return i.setCommonHeaders(s),s})).then((function(t){return i.fetch(g(s,i.domain,i.domainDelimiter)+e,t)})).then((function(e){return F(e)}))}},{key:"downloadRequest",value:function(e,t,r,s){var i=this;return this.auth.checkAndRefreshAccessToken().then((function(){if(r!==l)throw Error("Unexpected auth type: ".concat(r));var e={method:"POST",headers:{Authorization:"Bearer ".concat(i.auth.getAccessToken()),"Dropbox-API-Arg":k(t)}};return i.setCommonHeaders(e),e})).then((function(t){return i.fetch(g(s,i.domain,i.domainDelimiter)+e,t)})).then((function(e){return function(e){return e.ok?new Promise((function(t){A()?e.blob().then((function(e){return t(e)})):e.buffer().then((function(e){return t(e)}))})).then((function(t){var r=JSON.parse(e.headers.get("dropbox-api-result"));return A()?r.fileBlob=t:r.fileBinary=t,new P(e.status,e.headers,r)})):R(e)}(e)}))}},{key:"uploadRequest",value:function(e,t,r,s){var i=this;return this.auth.checkAndRefreshAccessToken().then((function(){if(r!==l)throw Error("Unexpected auth type: ".concat(r));var e=t.contents;delete t.contents;var s={body:e,method:"POST",headers:{Authorization:"Bearer ".concat(i.auth.getAccessToken()),"Content-Type":"application/octet-stream","Dropbox-API-Arg":k(t)}};return i.setCommonHeaders(s),s})).then((function(t){return i.fetch(g(s,i.domain,i.domainDelimiter)+e,t)})).then((function(e){return F(e)}))}},{key:"setCommonHeaders",value:function(e){this.selectUser&&(e.headers["Dropbox-API-Select-User"]=this.selectUser),this.selectAdmin&&(e.headers["Dropbox-API-Select-Admin"]=this.selectAdmin),this.pathRoot&&(e.headers["Dropbox-API-Path-Root"]=this.pathRoot)}}]),e}(),e.DropboxAuth=E,e.DropboxResponse=P,e.DropboxResponseError=M,Object.defineProperty(e,"__esModule",{value:!0})}));
diff --git a/procedural/src/viewer/libs/indexedDB.js b/procedural/src/viewer/libs/indexedDB.js
new file mode 100644
index 00000000..37e89c13
--- /dev/null
+++ b/procedural/src/viewer/libs/indexedDB.js
@@ -0,0 +1,65 @@
+let db;
+
+const DATABASE_NAME = "d2";
+const STORE_NAME = "s";
+
+const openDatabase = () => {
+ return new Promise((resolve, reject) => {
+ if (db) resolve();
+
+ if (!window.indexedDB) return reject("IndexedDB is not supported");
+ const request = window.indexedDB.open(DATABASE_NAME);
+
+ request.onsuccess = event => {
+ db = event.target.result;
+ resolve();
+ };
+
+ request.onerror = event => {
+ console.error("IndexedDB request error");
+ reject();
+ };
+
+ request.onupgradeneeded = event => {
+ db = event.target.result;
+ const objectStore = db.createObjectStore(STORE_NAME, {keyPath: "key"});
+ objectStore.transaction.oncomplete = () => {
+ db = event.target.result;
+ };
+ };
+ });
+};
+
+const ldb = {
+ get: key => {
+ return new Promise((resolve, reject) => {
+ openDatabase().then(() => {
+ const hasStore = Array.from(db.objectStoreNames).includes(STORE_NAME);
+ if (!hasStore) return reject("IndexedDB: no store found");
+
+ const transaction = db.transaction(STORE_NAME, "readonly");
+ const objectStore = transaction.objectStore(STORE_NAME);
+ const getRequest = objectStore.get(key);
+
+ getRequest.onsuccess = event => {
+ const result = event.target.result?.value || null;
+ resolve(result);
+ };
+ });
+ });
+ },
+
+ set: (keyName, value) => {
+ return new Promise(resolve => {
+ openDatabase().then(() => {
+ const transaction = db.transaction(STORE_NAME, "readwrite");
+ const objectStore = transaction.objectStore([STORE_NAME]);
+ const putRequest = objectStore.put({key: keyName, value});
+
+ putRequest.onsuccess = () => {
+ resolve();
+ };
+ });
+ });
+ }
+};
diff --git a/procedural/src/viewer/libs/jquery-3.1.1.min.js b/procedural/src/viewer/libs/jquery-3.1.1.min.js
new file mode 100644
index 00000000..4c5be4c0
--- /dev/null
+++ b/procedural/src/viewer/libs/jquery-3.1.1.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML=" ",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML=" ";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML=" ","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML=" ",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
+a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/