massive rework continues

This commit is contained in:
barrulus 2025-08-04 13:35:23 -04:00
parent d1b07fff01
commit 37391c8e8b
129 changed files with 4080 additions and 22216 deletions

195
procedural/TREE.md Normal file
View file

@ -0,0 +1,195 @@
procedural
├── .gitignore
├── NEXT_STEPS.md
├── PORT_PLAN.md
├── TREE.md
├── cli.js
├── index.html
├── main.js
├── package-lock.json
├── package.json
├── public
│ ├── assets
│ │ ├── charges
│ │ │ ├── agnusDei.svg
│ │ │ ├── anchor.svg
│ │ │ ├── angel.svg
│ │ │ ├── annulet.svg
.
.
.
│ │ │ ├── wolfHeadErased.svg
│ │ │ ├── wolfPassant.svg
│ │ │ ├── wolfRampant.svg
│ │ │ ├── wolfStatant.svg
│ │ │ ├── wyvern.svg
│ │ │ └── wyvernWithWingsDisplayed.svg
│ │ ├── heightmaps
│ │ │ ├── africa-centric.png
│ │ │ ├── arabia.png
│ │ │ ├── atlantics.png
│ │ │ ├── britain.png
│ │ │ ├── caribbean.png
│ │ │ ├── east-asia.png
│ │ │ ├── eurasia.png
│ │ │ ├── europe-accented.png
│ │ │ ├── europe-and-central-asia.png
│ │ │ ├── europe-central.png
│ │ │ ├── europe-north.png
│ │ │ ├── europe.png
│ │ │ ├── greenland.png
│ │ │ ├── hellenica.png
│ │ │ ├── iceland.png
│ │ │ ├── import-rules.txt
│ │ │ ├── indian-ocean.png
│ │ │ ├── mediterranean-sea.png
│ │ │ ├── middle-east.png
│ │ │ ├── north-america.png
│ │ │ ├── us-centric.png
│ │ │ ├── us-mainland.png
│ │ │ ├── world-from-pacific.png
│ │ │ └── world.png
│ │ └── images
│ │ ├── Discord.png
│ │ ├── Facebook.png
│ │ ├── Pinterest.png
│ │ ├── Reddit.png
│ │ ├── Twitter.png
│ │ ├── icons
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── icon_x512.png
│ │ │ ├── maskable_icon_x128.png
│ │ │ ├── maskable_icon_x192.png
│ │ │ ├── maskable_icon_x384.png
│ │ │ └── maskable_icon_x512.png
│ │ ├── kiwiroo.png
│ │ ├── pattern1.png
│ │ ├── pattern2.png
│ │ ├── pattern3.png
│ │ ├── pattern4.png
│ │ ├── pattern5.png
│ │ ├── pattern6.png
│ │ ├── preview.png
│ │ └── textures
│ │ ├── antique-big.jpg
│ │ ├── antique-small.jpg
│ │ ├── folded-paper-big.jpg
│ │ ├── folded-paper-small.jpg
│ │ ├── gray-paper.jpg
│ │ ├── iran-small.jpg
│ │ ├── marble-big.jpg
│ │ ├── marble-blue-big.jpg
│ │ ├── marble-blue-small.jpg
│ │ ├── marble-small.jpg
│ │ ├── mars-big.jpg
│ │ ├── mars-small.jpg
│ │ ├── mauritania-small.jpg
│ │ ├── mercury-big.jpg
│ │ ├── mercury-small.jpg
│ │ ├── ocean.jpg
│ │ ├── pergamena-small.jpg
│ │ ├── plaster.jpg
│ │ ├── soiled-paper-vertical.png
│ │ ├── soiled-paper.jpg
│ │ ├── spain-small.jpg
│ │ ├── timbercut-big.jpg
│ │ └── timbercut-small.jpg
│ └── vite.svg
├── src
│ ├── default_prompt.md
│ ├── engine
│ │ ├── main.js
│ │ ├── modules
│ │ │ ├── biomes.js
│ │ │ ├── burgs-and-states.js
│ │ │ ├── coa-generator.js
│ │ │ ├── coa-renderer.js
│ │ │ ├── cultures-generator.js
│ │ │ ├── features.js
│ │ │ ├── fonts.js
│ │ │ ├── heightmap-generator.js
│ │ │ ├── lakes.js
│ │ │ ├── markers-generator.js
│ │ │ ├── military-generator.js
│ │ │ ├── names-generator.js
│ │ │ ├── ocean-layers.js
│ │ │ ├── provinces-generator.js
│ │ │ ├── religions-generator.js
│ │ │ ├── resample.js
│ │ │ ├── river-generator.js
│ │ │ ├── routes-generator.js
│ │ │ ├── submap.js
│ │ │ ├── voronoi.js
│ │ │ └── zones-generator.js
│ │ └── utils
│ │ ├── alea.js
│ │ ├── arrayUtils.js
│ │ ├── cell.js
│ │ ├── colorUtils.js
│ │ ├── commonUtils.js
│ │ ├── debugUtils.js
│ │ ├── flatqueue.js
│ │ ├── functionUtils.js
│ │ ├── geography.js
│ │ ├── graphUtils.js
│ │ ├── index.js
│ │ ├── languageUtils.js
│ │ ├── lineclip.js
│ │ ├── nodeUtils.js
│ │ ├── numberUtils.js
│ │ ├── pathUtils.js
│ │ ├── polyfills.js
│ │ ├── polylabel.js
│ │ ├── probabilityUtils.js
│ │ ├── simplify.js
│ │ ├── stringUtils.js
│ │ └── unitUtils.js
│ ├── libs
│ │ └── delaunator.min.js
│ └── viewer
│ ├── _config_data
│ │ ├── biomes_config.md
│ │ ├── burgs-and-states_config.md
│ │ ├── coa-generator_config.md
│ │ ├── coa-renderer_config.md
│ │ ├── cultures-generator_config.md
│ │ ├── features_config.md
│ │ ├── fonts_config.md
│ │ ├── heightmap-generator_config.md
│ │ ├── lakes_config.md
│ │ ├── markers-generator_config.md
│ │ ├── military-generator.js_config.md
│ │ ├── names-generator_config.md
│ │ ├── ocean-layers_config.md
│ │ ├── provinces-generator.js_config.md
│ │ ├── religions-generator_config.md
│ │ ├── resample_config.md
│ │ ├── river-generator_config.md
│ │ ├── routes-generator_config.md
│ │ ├── submap_config.md
│ │ ├── voronoi_config.md
│ │ └── zones_config.md
│ ├── config-builder.js
│ ├── config-integration.md
│ ├── config-presets.js
│ ├── config-schema.md
│ ├── config-validator.js
│ ├── libs
│ │ ├── dropbox-sdk.min.js
│ │ ├── indexedDB.js
│ │ ├── jquery-3.1.1.min.js
│ │ ├── jquery-ui.css
│ │ ├── jquery-ui.min.js
│ │ ├── jquery.ui.touch-punch.min.js
│ │ ├── jszip.min.js
│ │ ├── loopsubdivison.min.js
│ │ ├── objexporter.min.js
│ │ ├── openwidget.min.js
│ │ ├── orbitControls.min.js
│ │ ├── rgbquant.min.js
│ │ ├── shorthands.js
│ │ ├── three.min.js
│ │ └── umami.js
│ └── main.js
└── style.css

42
procedural/cli.js Normal file
View file

@ -0,0 +1,42 @@
// cli.js
import { generate } from './src/engine/main.js';
import { getPreset } from './src/viewer/config-presets.js';
import { validateConfig, sanitizeConfig } from './src/viewer/config-validator.js';
import fs from 'fs';
// Load config from file
function loadConfig(filepath) {
const jsonString = fs.readFileSync(filepath, 'utf8');
return JSON.parse(jsonString);
}
// Generate from CLI
async function generateFromCLI(options) {
let config;
if (options.preset) {
config = getPreset(options.preset);
} else if (options.config) {
config = loadConfig(options.config);
} else {
config = getPreset('default');
}
// Override with CLI arguments
if (options.seed) config.seed = options.seed;
if (options.width) config.graph.width = parseInt(options.width);
if (options.height) config.graph.height = parseInt(options.height);
// Validate and fix
const validation = validateConfig(config);
if (!validation.valid) {
console.warn('Config validation warnings:', validation.warnings);
config = sanitizeConfig(config);
}
// Generate
const mapData = generate(config);
// Save output
fs.writeFileSync(options.output || 'map.json', JSON.stringify(mapData));
}

View file

@ -1,37 +1,308 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fantasy Map Generator - Procedural Engine</title>
<link rel="stylesheet" href="/style.css" />
</head>
<head> <body>
<meta charset="UTF-8" /> <div id="app">
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <!-- You can change this later --> <!-- Configuration Panel -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <div id="optionsContainer">
<title>Fantasy Map Generator (Vite)</title> <h2>Map Configuration</h2>
<link rel="stylesheet" href="/style.css" />
</head>
<body> <!-- Preset Selector -->
<div id="app"> <div class="config-section">
<!-- All your FMG UI elements (options panel, buttons, etc.) will go here --> <label for="presetSelector">Preset:</label>
<div id="optionsContainer"> <select id="presetSelector">
<!-- e.g., <input type="text" id="optionsSeed" /> --> <option value="custom">Custom</option>
<!-- Add inside #optionsContainer in index.html --> </select>
<input type="text" id="optionsSeed" placeholder="Enter seed" /> </div>
<button id="generateMapButton">Generate Map</button>
<button id="newMapButton">New Map</button> <!-- Core Settings -->
<div class="config-section">
<h3>Core Settings</h3>
<label for="optionsSeed">Seed:</label>
<input type="text" id="optionsSeed" placeholder="Auto-generate" />
<input type="text" id="seed" placeholder="Alternative seed input" />
<label for="mapWidthInput">Width:</label>
<input type="number" id="mapWidthInput" value="1920" min="100" max="8192" />
<label for="mapHeightInput">Height:</label>
<input type="number" id="mapHeightInput" value="1080" min="100" max="8192" />
</div>
<!-- Graph Settings -->
<div class="config-section">
<h3>Graph Settings</h3>
<label for="pointsInput">Cells:</label>
<input type="number" id="pointsInput" value="10000" min="1000" max="100000" data-cells="10000" />
<label for="cellsNumber">Cell Count:</label>
<input type="number" id="cellsNumber" value="10000" readonly />
<label for="pointsNumber">Points:</label>
<input type="number" id="pointsNumber" value="10000" readonly />
<label for="boundary">Boundary:</label>
<select id="boundary">
<option value="box">Box</option>
<option value="circle">Circle</option>
</select>
</div>
<!-- Heightmap Settings -->
<div class="config-section">
<h3>Heightmap</h3>
<label for="templateInput">Template:</label>
<select id="templateInput">
<option value="continents">Continents</option>
<option value="archipelago">Archipelago</option>
<option value="highland">Highland</option>
<option value="inland">Inland</option>
<option value="lakes">Lakes</option>
<option value="islands">Islands</option>
<option value="atoll">Atoll</option>
<option value="volcano">Volcano</option>
<option value="crater">Crater</option>
</select>
</div>
<!-- Temperature Settings -->
<div class="config-section">
<h3>Temperature</h3>
<label for="temperatureEquatorOutput">Equator:</label>
<input type="number" id="temperatureEquatorOutput" value="30" />
<label for="temperatureNorthPoleOutput">North Pole:</label>
<input type="number" id="temperatureNorthPoleOutput" value="-10" />
<label for="temperatureSouthPoleOutput">South Pole:</label>
<input type="number" id="temperatureSouthPoleOutput" value="-15" />
<label for="heightExponentInput">Height Exponent:</label>
<input type="number" id="heightExponentInput" value="1.8" min="0.5" max="5" step="0.1" />
<label for="temperatureScale">Scale:</label>
<select id="temperatureScale">
<option value="C">Celsius</option>
<option value="F">Fahrenheit</option>
</select>
<label for="temperatureBase">Base Temp:</label>
<input type="number" id="temperatureBase" value="25" />
</div>
<!-- Precipitation Settings -->
<div class="config-section">
<h3>Precipitation</h3>
<label for="precInput">Precipitation:</label>
<input type="number" id="precInput" value="100" />
<label for="moisture">Moisture:</label>
<input type="number" id="moisture" value="1" min="0.1" max="2" step="0.1" />
</div>
<!-- Map Settings -->
<div class="config-section">
<h3>Map Settings</h3>
<label for="coordinatesSize">Coordinate Size:</label>
<input type="number" id="coordinatesSize" value="1" min="0.1" max="10" step="0.1" />
<label for="latitude">Latitude:</label>
<input type="number" id="latitude" value="0" min="-90" max="90" />
</div>
<!-- Lakes Settings -->
<div class="config-section">
<h3>Lakes</h3>
<label for="lakeElevationLimitOutput">Elevation Limit:</label>
<input type="number" id="lakeElevationLimitOutput" value="50" min="0" max="100" />
</div>
<!-- Rivers Settings -->
<div class="config-section">
<h3>Rivers</h3>
<label for="resolveDepressionsStepsOutput">Depression Steps:</label>
<input type="number" id="resolveDepressionsStepsOutput" value="1000" min="100" max="10000" />
</div>
<!-- Ocean Layers -->
<div class="config-section">
<h3>Ocean</h3>
<div id="oceanLayers" layers="-1,-2,-3"></div>
</div>
<!-- Cultures Settings -->
<div class="config-section">
<h3>Cultures</h3>
<label for="culturesInput">Number of Cultures:</label>
<input type="number" id="culturesInput" value="12" min="0" max="99" />
<label for="culturesSet">Culture Set:</label>
<select id="culturesSet">
<option value="european" data-max="15">European</option>
<option value="oriental" data-max="20">Oriental</option>
<option value="english" data-max="10">English</option>
<option value="antique" data-max="12">Antique</option>
<option value="highFantasy" data-max="30">High Fantasy</option>
<option value="darkFantasy" data-max="20">Dark Fantasy</option>
<option value="random" data-max="25">Random</option>
<option value="all-world" data-max="20">All World</option>
</select>
<label for="emblemShape">Emblem Shape:</label>
<select id="emblemShape">
<option value="random">Random</option>
<optgroup label="Basic">
<option value="heater">Heater</option>
<option value="spanish">Spanish</option>
<option value="french">French</option>
<option value="round">Round</option>
<option value="oval">Oval</option>
<option value="square">Square</option>
</optgroup>
<optgroup label="Historical">
<option value="roman">Roman</option>
<option value="kite">Kite</option>
<option value="oldFrench">Old French</option>
<option value="renaissance">Renaissance</option>
<option value="baroque">Baroque</option>
</optgroup>
<optgroup label="Fantasy">
<option value="fantasy1">Fantasy Style 1</option>
<option value="fantasy2">Fantasy Style 2</option>
<option value="fantasy3">Fantasy Style 3</option>
<option value="gothic">Gothic</option>
</optgroup>
<optgroup label="Diversiform">
<option value="flag">Flag</option>
<option value="pennon">Pennon</option>
<option value="banner">Banner</option>
</optgroup>
</select>
<label for="neutralRate">Neutral Rate:</label>
<input type="number" id="neutralRate" value="1" min="0.1" max="10" step="0.1" />
</div>
<!-- States & Burgs Settings -->
<div class="config-section">
<h3>States & Burgs</h3>
<label for="statesNumber">Number of States:</label>
<input type="number" id="statesNumber" value="15" min="0" max="999" />
<label for="manorsInput">Number of Towns:</label>
<input type="number" id="manorsInput" value="1000" min="0" max="10000" title="1000 = auto-calculate" />
<label for="sizeVariety">Size Variety:</label>
<input type="number" id="sizeVariety" value="1" min="0" max="5" step="0.1" />
<label for="growthRate">Growth Rate:</label>
<input type="number" id="growthRate" value="1" min="0.1" max="10" step="0.1" />
<label for="statesGrowthRate">States Growth Rate:</label>
<input type="number" id="statesGrowthRate" value="1" min="0.1" max="10" step="0.1" />
</div>
<!-- Religions Settings -->
<div class="config-section">
<h3>Religions</h3>
<label for="religionsNumber">Number of Religions:</label>
<input type="number" id="religionsNumber" value="5" min="0" max="99" />
</div>
<!-- Provinces Settings -->
<div class="config-section">
<h3>Provinces</h3>
<label for="provincesRatio">Provinces Ratio:</label>
<input type="number" id="provincesRatio" value="50" min="0" max="100" />
</div>
<!-- Military Settings -->
<div class="config-section">
<h3>Military</h3>
<label for="year">Year:</label>
<input type="number" id="year" value="1400" />
<label for="eraShort">Era (Short):</label>
<input type="text" id="eraShort" value="AD" />
<label for="era">Era:</label>
<input type="text" id="era" value="Anno Domini" />
</div>
<!-- Zones Settings -->
<div class="config-section">
<h3>Zones</h3>
<label for="zonesGlobalModifier">Global Modifier:</label>
<input type="number" id="zonesGlobalModifier" value="1" min="0.1" max="10" step="0.1" />
</div>
<!-- Control Buttons -->
<div class="config-section controls">
<h3>Actions</h3>
<button id="newMapButton" class="primary">🗺️ Generate Map</button>
<button id="generateButton" class="primary">Generate (Alt)</button>
<div class="button-group">
<button id="saveConfigButton">💾 Save Config</button>
<label for="loadConfigInput" class="button">📁 Load Config</label>
<input type="file" id="loadConfigInput" accept=".json" style="display: none;" />
</div>
<label>
<input type="checkbox" id="restoreSession" checked />
Restore last session
</label>
</div>
<!-- Status/Info -->
<div class="config-section">
<h3>Info</h3>
<div id="status">Ready</div>
<div id="shortcuts">
<small>
<strong>Shortcuts:</strong><br/>
Ctrl+G - Generate Map<br/>
Ctrl+Shift+S - Save Config
</small>
</div>
</div>
</div>
<!-- The main SVG map container -->
<svg id="map" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs id="deftemp"></defs>
<g id="viewbox"></g>
<!-- Additional SVG groups will be added by the renderer -->
</svg>
</div> </div>
<!-- The main SVG map container --> <!-- Custom prompt dialog (required by commonUtils.js) -->
<svg id="map" version="1.1" xmlns="http://www.w3.org/2000/svg"> <div id="prompt" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 10000;">
<defs id="deftemp"></defs> <form id="promptForm">
<g id="viewbox"></g> <div id="promptText">Please provide an input</div>
<!-- ... other static SVG groups --> <input type="text" id="promptInput" style="margin: 10px 0; width: 100%;" />
</svg> <button type="submit">OK</button>
</div> <button type="button" id="promptCancel">Cancel</button>
<!-- Comment out the old script loading order --> </form>
<!-- <script type="module" src="/main.js"></script> --> </div>
<!-- Add the new entry point --> <!-- Load the viewer with config system -->
<script type="module" src="src/viewer/main.js"></script> <script type="module" src="src/viewer/main.js"></script>
</body>
<!-- Helper function for backwards compatibility -->
<script>
// Global byId helper for modules that expect it
window.byId = function(id) {
return document.getElementById(id);
};
</script>
</body>
</html> </html>

View file

@ -7,6 +7,11 @@
"": { "": {
"name": "procedural", "name": "procedural",
"version": "0.0.0", "version": "0.0.0",
"dependencies": {
"aleaprng": "^1.0.1",
"d3": "^5.16.0",
"delaunator": "^5.0.1"
},
"devDependencies": { "devDependencies": {
"vite": "^7.0.4" "vite": "^7.0.4"
} }
@ -740,34 +745,323 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-delaunay": { "node_modules/aleaprng": {
"version": "6.0.4", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "resolved": "https://registry.npmjs.org/aleaprng/-/aleaprng-1.0.1.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "integrity": "sha512-bbhVl2gniAT4Szfl+UtPM9NXM+yaC73pe4fq7AL2NO+htYT0PSUgxNa4vbKj+AiFAiXrQQrSR6YgjGwExB5Q6Q==",
"license": "ISC", "license": "BSD-3-Clause"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/d3": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz",
"integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"delaunator": "5" "d3-array": "1",
}, "d3-axis": "1",
"engines": { "d3-brush": "1",
"node": ">=12" "d3-chord": "1",
"d3-collection": "1",
"d3-color": "1",
"d3-contour": "1",
"d3-dispatch": "1",
"d3-drag": "1",
"d3-dsv": "1",
"d3-ease": "1",
"d3-fetch": "1",
"d3-force": "1",
"d3-format": "1",
"d3-geo": "1",
"d3-hierarchy": "1",
"d3-interpolate": "1",
"d3-path": "1",
"d3-polygon": "1",
"d3-quadtree": "1",
"d3-random": "1",
"d3-scale": "2",
"d3-scale-chromatic": "1",
"d3-selection": "1",
"d3-shape": "1",
"d3-time": "1",
"d3-time-format": "2",
"d3-timer": "1",
"d3-transition": "1",
"d3-voronoi": "1",
"d3-zoom": "1"
} }
}, },
"node_modules/d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==",
"license": "BSD-3-Clause"
},
"node_modules/d3-axis": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-brush": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz",
"integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"node_modules/d3-chord": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "1",
"d3-path": "1"
}
},
"node_modules/d3-collection": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==",
"license": "BSD-3-Clause"
},
"node_modules/d3-color": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==",
"license": "BSD-3-Clause"
},
"node_modules/d3-contour": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^1.1.1"
}
},
"node_modules/d3-dispatch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-drag": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz",
"integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"node_modules/d3-dsv": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz",
"integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==",
"license": "BSD-3-Clause",
"dependencies": {
"commander": "2",
"iconv-lite": "0.4",
"rw": "1"
},
"bin": {
"csv2json": "bin/dsv2json",
"csv2tsv": "bin/dsv2dsv",
"dsv2dsv": "bin/dsv2dsv",
"dsv2json": "bin/dsv2json",
"json2csv": "bin/json2dsv",
"json2dsv": "bin/json2dsv",
"json2tsv": "bin/json2dsv",
"tsv2csv": "bin/dsv2dsv",
"tsv2json": "bin/dsv2json"
}
},
"node_modules/d3-ease": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz",
"integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-fetch": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz",
"integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dsv": "1"
}
},
"node_modules/d3-force": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
"integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-collection": "1",
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
}
},
"node_modules/d3-format": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-geo": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz",
"integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "1"
}
},
"node_modules/d3-hierarchy": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
"integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-interpolate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1"
}
},
"node_modules/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
"license": "BSD-3-Clause"
},
"node_modules/d3-polygon": { "node_modules/d3-polygon": {
"version": "3.0.1", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz",
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==",
"license": "ISC", "license": "BSD-3-Clause"
"engines": {
"node": ">=12"
}
}, },
"node_modules/d3-quadtree": { "node_modules/d3-quadtree": {
"version": "3.0.1", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==",
"license": "ISC", "license": "BSD-3-Clause"
"engines": { },
"node": ">=12" "node_modules/d3-random": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-scale": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^1.2.0",
"d3-collection": "1",
"d3-format": "1",
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
}
},
"node_modules/d3-scale-chromatic": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
"integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1",
"d3-interpolate": "1"
}
},
"node_modules/d3-selection": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz",
"integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==",
"license": "BSD-3-Clause"
},
"node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "1"
}
},
"node_modules/d3-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-time-format": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
"integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-time": "1"
}
},
"node_modules/d3-timer": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
"integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==",
"license": "BSD-3-Clause"
},
"node_modules/d3-transition": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
"integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1",
"d3-dispatch": "1",
"d3-ease": "1",
"d3-interpolate": "1",
"d3-selection": "^1.1.0",
"d3-timer": "1"
}
},
"node_modules/d3-voronoi": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==",
"license": "BSD-3-Clause"
},
"node_modules/d3-zoom": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
"integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
} }
}, },
"node_modules/delaunator": { "node_modules/delaunator": {
@ -836,15 +1130,6 @@
} }
} }
}, },
"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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -860,6 +1145,18 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -974,6 +1271,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View file

@ -10,5 +10,10 @@
}, },
"devDependencies": { "devDependencies": {
"vite": "^7.0.4" "vite": "^7.0.4"
},
"dependencies": {
"aleaprng": "^1.0.1",
"d3": "^5.16.0",
"delaunator": "^5.0.1"
} }
} }

View file

@ -17,11 +17,11 @@ import * as Religions from "./modules/religions-generator.js";
import * as Rivers from "./modules/river-generator.js"; import * as Rivers from "./modules/river-generator.js";
import * as Routes from "./modules/routes-generator.js"; import * as Routes from "./modules/routes-generator.js";
import * as Zones from "./modules/zones-generator.js"; import * as Zones from "./modules/zones-generator.js";
import { Voronoi } from "./modules/voronoi.js"; import * as voronoi from "./modules/voronoi.js";
import * as Utils from "./utils/index.js"; import * as Utils from "./utils/index.js";
// Import the new utility modules // Import the new utility modules
import * as Graph from "./utils/graphUtils.js"; import * as Graph from "./utils/graph.js";
import * as Geography from "./utils/geography.js"; import * as Geography from "./utils/geography.js";
import * as Cell from "./utils/cell.js"; import * as Cell from "./utils/cell.js";
@ -31,66 +31,101 @@ import * as Cell from "./utils/cell.js";
* @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }. * @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }.
*/ */
export function generate(config) { export function generate(config) {
const timeStart = performance.now(); const timeStart = performance.now();
const { TIME, WARN, INFO, ERROR } = Utils; // Core logging utils
// Set up PRNG // CORRECT: Get debug flags (values) from the config object.
const seed = config.seed || Utils.generateSeed(); const { TIME, WARN, INFO } = config.debug;
Math.random = Utils.aleaPRNG(seed);
INFO && console.group("Generating Map with Seed: " + seed);
// --- Grid Generation --- // CORRECT: Call utility functions directly from the Utils object.
let grid = Graph.generateGrid(config.graph); const seed = config.seed || Utils.generateSeed();
grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils); Math.random = Utils.aleaPRNG(seed);
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 --- // This now works, because INFO is a boolean from config.debug
const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils); INFO && console.group("Generating Map with Seed: " + seed);
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 --- // 2. Pass the 'graph' section of the config to the new graph utilities
const riverGenerationResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names }); let grid = Graph.generateGrid(config.graph);
pack = riverGenerationResult.pack;
// --- Biome and Population --- // --- Heightmap and Features (assumed to be already modular) ---
const { biome } = Biomes.define(pack, grid, config.biomes, Utils); grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils);
pack.cells.biome = biome; grid = Features.markupGrid(grid, config, Utils);
const { s, pop } = Cell.rankCells(pack, grid, Utils, { biomesData: Biomes.getDefault() });
pack.cells.s = s;
pack.cells.pop = pop;
// --- Cultures, States, and Burgs --- // 3. Pass 'map' and 'lakes' configs to the new geography utilities
const culturesResult = Cultures.generate(pack, grid, config.cultures, Utils, { Names }); const { mapCoordinates } = Geography.defineMapSize(grid, config, Utils);
pack.cultures = culturesResult.cultures; grid = Geography.addLakesInDeepDepressions(grid, config.lakes, Utils);
pack.cells.culture = culturesResult.culture; grid = Geography.openNearSeaLakes(grid, config.lakes, Utils);
Cultures.expand(pack, config.cultures, Utils, { biomesData: Biomes.getDefault() });
const burgsAndStatesResult = BurgsAndStates.generate(pack, grid, config.burgs, Utils, { Names, COA, getPolesOfInaccessibility: Utils.getPolesOfInaccessibility }); // 4. Pass specific config sections for temperature and precipitation
pack.burgs = burgsAndStatesResult.burgs; const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils);
pack.states = burgsAndStatesResult.states; grid.cells.temp = temp;
pack.cells.burg = burgsAndStatesResult.cells.burg; const { prec } = Geography.generatePrecipitation(grid, mapCoordinates, config.precipitation, Utils);
pack.cells.state = burgsAndStatesResult.cells.state; 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;
// 5. Call the new cell ranking utility
const { s, pop } = Cell.rankCells(pack, Utils, { biomesData: Biomes.getDefault() });
pack.cells.s = s;
pack.cells.pop = pop;
// 6. Cultures, States, and Burgs
const culturesResult = Cultures.generate(pack, grid, config.cultures, Utils, { Names });
let packWithCultures = { ...pack, cultures: culturesResult.cultures };
packWithCultures.cells.culture = culturesResult.culture;
const expandedCulturesData = Cultures.expand(packWithCultures, config.cultures, Utils, { biomesData: Biomes.getDefault() });
pack = { ...packWithCultures, ...expandedCulturesData }; // Assumes expand returns an object with updated pack properties
const burgsAndStatesResult = BurgsAndStates.generate(pack, grid, config.burgs, Utils, { Names, COA });
pack = {
...pack,
burgs: burgsAndStatesResult.burgs,
states: burgsAndStatesResult.states
};
pack.cells.burg = burgsAndStatesResult.burg;
pack.cells.state = burgsAndStatesResult.state;
const routesResult = Routes.generate(pack, Utils);
pack = { ...pack, ...routesResult }; // Merge new routes data
const religionsResult = Religions.generate(pack, config.religions, Utils, { Names, BurgsAndStates });
pack = { ...pack, ...religionsResult }; // Merge new religions data
const stateFormsResult = BurgsAndStates.defineStateForms(pack, Utils, { Names });
pack = { ...pack, ...stateFormsResult }; // Merge updated state forms
const provincesResult = Provinces.generate(pack, config.provinces, Utils, { BurgsAndStates, Names, COA });
pack = { ...pack, ...provincesResult }; // Merge new provinces data
const burgFeaturesResult = BurgsAndStates.defineBurgFeatures(pack, Utils);
pack = { ...pack, ...burgFeaturesResult }; // Merge updated burg features
const specifiedRiversResult = Rivers.specify(pack, Utils, { Names });
pack = { ...pack, ...specifiedRiversResult }; // Merge specified river data
const specifiedFeaturesResult = Features.specify(pack, grid, Utils, { Lakes });
pack = { ...pack, ...specifiedFeaturesResult }; // Merge specified feature data
const militaryResult = Military.generate(pack, config.military, Utils, { Names });
pack = { ...pack, ...militaryResult }; // Merge new military data
const markersResult = Markers.generate(pack, config.markers, Utils);
pack = { ...pack, ...markersResult }; // Merge new markers data
const zonesResult = Zones.generate(pack, config.zones, Utils);
pack = { ...pack, ...zonesResult }; // Merge new zones data
// --- 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`); WARN && console.warn(`TOTAL GENERATION TIME: ${Utils.rn((performance.now() - timeStart) / 1000, 2)}s`);
INFO && console.groupEnd("Generated Map " + seed); INFO && console.groupEnd("Generated Map " + seed);

View file

@ -53,7 +53,7 @@ export const getDefault = () => {
]; ];
const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
const biomesMartix = [ const biomesMartix = [
// hot ” cold [>19°C; <-4°C]; dry • wet // hot <EFBFBD> cold [>19<31>C; <-4<>C]; dry <20> 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([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([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, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]),
@ -77,7 +77,8 @@ export const getDefault = () => {
// assign biome id for each cell // assign biome id for each cell
export function define(pack, grid, config, utils) { export function define(pack, grid, config, utils) {
const {TIME, d3, rn} = utils; const { d3, rn} = utils;
const { TIME } = config.debug;
TIME && console.time("defineBiomes"); TIME && console.time("defineBiomes");
const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells; const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;

View file

@ -36,7 +36,8 @@ export const generate = (pack, grid, config, utils) => {
}; };
function placeCapitals(pack, grid, config, utils) { function placeCapitals(pack, grid, config, utils) {
const {TIME, WARN, d3, graphWidth, graphHeight} = utils; const { WARN, d3, graphWidth, graphHeight} = utils;
const { TIME } = config.debug;
TIME && console.time("placeCapitals"); TIME && console.time("placeCapitals");
let count = config.statesNumber; let count = config.statesNumber;
let burgs = [0]; let burgs = [0];

View file

@ -1,7 +1,8 @@
"use strict"; "use strict";
export const generate = function (pack, grid, config, utils) { export const generate = function (pack, grid, config, utils) {
const { TIME, WARN, ERROR, rand, rn, P, minmax, biased, rw, abbreviate } = utils; const { WARN, ERROR, rand, rn, P, minmax, biased, rw, abbreviate } = utils;
const { TIME } = config.debug;
TIME && console.time("generateCultures"); TIME && console.time("generateCultures");
const cells = pack.cells; const cells = pack.cells;

View file

@ -26,11 +26,10 @@ function markup({distanceField, neighbors, start, increment, limit = utils.INT8_
// mark Grid features (ocean, lakes, islands) and calculate distance field // mark Grid features (ocean, lakes, islands) and calculate distance field
export function markupGrid(grid, config, utils) { export function markupGrid(grid, config, utils) {
const {TIME, seed, aleaPRNG} = config;
const {rn} = utils; const {rn} = utils;
const { TIME } = config.debug;
TIME && console.time("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 {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
const cellsNumber = i.length; const cellsNumber = i.length;

View file

@ -1,12 +1,12 @@
"use strict"; "use strict";
export async function generate(graph, config, utils) { export async function generate(graph, config, utils) {
const { aleaPRNG, heightmapTemplates, TIME } = utils; const { aleaPRNG, heightmapTemplates } = utils;
const { TIME } = config.debug;
const { templateId, seed } = config; const { templateId, seed } = config;
TIME && console.time("defineHeightmap"); TIME && console.time("defineHeightmap");
Math.random = aleaPRNG(seed);
const isTemplate = templateId in heightmapTemplates; const isTemplate = templateId in heightmapTemplates;
const heights = isTemplate const heights = isTemplate
? fromTemplate(graph, templateId, config, utils) ? fromTemplate(graph, templateId, config, utils)

View file

@ -58,7 +58,7 @@ export function getDefaultMarkersConfig(config, utils) {
} }
export function generateMarkers(pack, config, utils) { export function generateMarkers(pack, config, utils) {
const {TIME} = utils; const { TIME } = config.debug;
const markersConfig = getDefaultMarkersConfig(config, utils); const markersConfig = getDefaultMarkersConfig(config, utils);
const markers = []; const markers = [];
const notes = []; const notes = [];

View file

@ -1,7 +1,8 @@
"use strict"; "use strict";
export function generate(pack, config, utils, notes) { export function generate(pack, config, utils, notes) {
const {TIME, minmax, rn, ra, rand, gauss, si, nth, d3, populationRate, urbanization} = utils; const { minmax, rn, ra, rand, gauss, si, nth, d3, populationRate, urbanization} = utils;
const { TIME } = config.debug;
TIME && console.time("generateMilitary"); TIME && console.time("generateMilitary");
const {cells, states, burgs, provinces} = pack; const {cells, states, burgs, provinces} = pack;

View file

@ -11,7 +11,6 @@ const forms = {
export const generate = (pack, config, utils, regenerate = false, regenerateLockedStates = false) => { export const generate = (pack, config, utils, regenerate = false, regenerateLockedStates = false) => {
const { const {
TIME,
generateSeed, generateSeed,
aleaPRNG, aleaPRNG,
gauss, gauss,
@ -25,6 +24,7 @@ export const generate = (pack, config, utils, regenerate = false, regenerateLock
d3, d3,
rand rand
} = utils; } = utils;
const { TIME } = config.debug;
TIME && console.time("generateProvinces"); TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : config.seed; const localSeed = regenerate ? generateSeed() : config.seed;

View file

@ -452,7 +452,7 @@ const expansionismMap = {
}; };
export function generate(pack, grid, config, utils) { export function generate(pack, grid, config, utils) {
const {TIME} = utils; const { TIME } = config.debug;
TIME && console.time("generateReligions"); TIME && console.time("generateReligions");
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || []; const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
@ -627,7 +627,7 @@ function combineReligions(namedReligions, lockedReligions, utils) {
.map(religion => { .map(religion => {
// and filter their origins to locked religions // and filter their origins to locked religions
let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n));
if (newOrigin === []) newOrigin = [0]; if (newOrigin.length === 0) newOrigin = [0];
return {...religion, origins: newOrigin}; return {...religion, origins: newOrigin};
}) })
.sort((a, b) => a.i - b.i); .sort((a, b) => a.i - b.i);

View file

@ -1,12 +1,12 @@
"use strict"; "use strict";
export const generate = function (pack, grid, config, utils, modules, allowErosion = true) { export const generate = function (pack, grid, config, utils, modules, allowErosion = true) {
const {TIME, seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config; const { seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config;
const {rn, rw, each, round, d3, lineGen} = utils; const {rn, rw, each, round, d3, lineGen} = utils;
const {Lakes, Names} = modules; const {Lakes, Names} = modules;
const { TIME } = config.debug;
TIME && console.time("generateRivers"); TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed);
const {cells, features} = pack; const {cells, features} = pack;
const riversData = {}; // rivers data const riversData = {}; // rivers data

View file

@ -1,129 +0,0 @@
"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 <20> cold [>19<31>C; <-4<>C]; dry <20> 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;
}

View file

@ -1,33 +0,0 @@
# 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

View file

@ -1,209 +0,0 @@
# 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 `<path>` 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.

View file

@ -1,9 +0,0 @@
# 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.

View file

@ -1,893 +0,0 @@
"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}`;
};

View file

@ -1,90 +0,0 @@
# 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);
```

View file

@ -1,967 +0,0 @@
# 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 `<path>` 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

View file

@ -1,35 +0,0 @@
# 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.

View file

@ -1,32 +0,0 @@
# 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

File diff suppressed because it is too large Load diff

View file

@ -1,48 +0,0 @@
# 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.

View file

@ -1,39 +0,0 @@
# 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": "<g>...</g>", // The raw <g> tag content of the charge's SVG
"chargeName2": "<g>...</g>",
// etc.
}
```
Example:
```javascript
const chargesData = {
"lion": '<g><path d="..."/></g>',
"eagle": '<g><path d="..."/></g>'
};
```
## 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.

File diff suppressed because it is too large Load diff

View file

@ -1,74 +0,0 @@
# 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 = `<use data-i="${i}" x="${x - size / 2}" y="${y - size / 2}" width="1em" height="1em" href="#${id}"/>`;
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 <use> element referencing the generated CoA, and inserts it. It also depends on another UI function (layerIsOn). This is quintessential Viewer logic.

View file

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

View file

@ -1,38 +0,0 @@
# 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

View file

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

View file

@ -1,99 +0,0 @@
# 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.<br />
No cultures, states and burgs will be created.<br />
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
```
**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.<br />
Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br />
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
```
**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

View file

@ -1,302 +0,0 @@
"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";
}
}

View file

@ -1,51 +0,0 @@
# 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 });
```

View file

@ -1,353 +0,0 @@
# 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 `<path>` 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

View file

@ -1,46 +0,0 @@
# 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 `<path>` 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.

View file

@ -1,16 +0,0 @@
# 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

View file

@ -1,477 +0,0 @@
# 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 `<path>` 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

View file

@ -1,134 +0,0 @@
# 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.

View file

@ -1,542 +0,0 @@
"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);
}

View file

@ -1,25 +0,0 @@
# 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

View file

@ -1,625 +0,0 @@
# 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 `<path>` 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.

View file

@ -1,18 +0,0 @@
# 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.

View file

@ -1,39 +0,0 @@
# 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

View file

@ -1,200 +0,0 @@
# 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 `<path>` 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

View file

@ -1,41 +0,0 @@
# 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.

View file

@ -1,40 +0,0 @@
# 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.

File diff suppressed because it is too large Load diff

View file

@ -1,51 +0,0 @@
# 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

View file

@ -1,23 +0,0 @@
# 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.

View file

@ -1,20 +0,0 @@
# 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.

View file

@ -1,478 +0,0 @@
# 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 `<path>` 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

View file

@ -1,24 +0,0 @@
# 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

View file

@ -1,371 +0,0 @@
# 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 `<path>` 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

View file

@ -1,46 +0,0 @@
# 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

View file

@ -1,92 +0,0 @@
"use strict";
window.OceanLayers = (function () {
let cells, vertices, pointsN, used;
const OceanLayers = function OceanLayers() {
const outline = oceanLayers.attr("layers");
if (outline === "none") return;
TIME && console.time("drawOceanLayers");
lineGen.curve(d3.curveBasisClosed);
(cells = grid.cells), (pointsN = grid.cells.i.length), (vertices = grid.vertices);
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
const chains = [];
const opacity = rn(0.4 / limits.length, 2);
used = new Uint8Array(pointsN); // to detect already passed cells
for (const i of cells.i) {
const t = cells.t[i];
if (t > 0) continue;
if (used[i] || !limits.includes(t)) continue;
const start = findStart(i, t);
if (!start) continue;
used[i] = 1;
const chain = connectVertices(start, t); // vertices chain to form a path
if (chain.length < 4) continue;
const relax = 1 + t * -2; // select only n-th point
const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN));
if (relaxed.length < 4) continue;
const points = clipPoly(
relaxed.map(v => vertices.p[v]),
1
);
chains.push([t, points]);
}
for (const t of limits) {
const layer = chains.filter(c => c[0] === t);
let path = layer.map(c => round(lineGen(c[1]))).join("");
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity);
}
// find eligible cell vertex to start path detection
function findStart(i, t) {
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= pointsN)); // map border cell
return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])];
}
TIME && console.timeEnd("drawOceanLayers");
};
function randomizeOutline() {
const limits = [];
let odd = 0.2;
for (let l = -9; l < 0; l++) {
if (P(odd)) {
odd = 0.2;
limits.push(l);
} else {
odd *= 2;
}
}
return limits;
}
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.t[c] === t).forEach(c => (used[c] = 1));
const v = vertices.v[current]; // neighboring vertices
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1;
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1;
const c2 = !cells.t[c[2]] || cells.t[c[2]] === t - 1;
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
chain.push(chain[0]); // push first vertex as the last one
return chain;
}
return OceanLayers;
})();

View file

@ -1,94 +0,0 @@
"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;
}

View file

@ -1,17 +0,0 @@
# 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.

View file

@ -1,176 +0,0 @@
# 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 `<path>` 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

View file

@ -1,55 +0,0 @@
# 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 `<path>` 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
}
]
}
```

View file

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

View file

@ -1,33 +0,0 @@
# 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)

View file

@ -1,23 +0,0 @@
# 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`).

View file

@ -1,341 +0,0 @@
# 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 `<path>` 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

View file

@ -1,410 +0,0 @@
"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;
}

View file

@ -1,33 +0,0 @@
# 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.

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
# 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.

View file

@ -1,40 +0,0 @@
# 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

View file

@ -1,451 +0,0 @@
# 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 `<path>` 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

View file

@ -1,30 +0,0 @@
# 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

View file

@ -1,559 +0,0 @@
"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;
};

View file

@ -1,95 +0,0 @@
# 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);
```

View file

@ -1,602 +0,0 @@
# 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 `<path>` 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

View file

@ -1,100 +0,0 @@
# 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.

View file

@ -1,712 +0,0 @@
const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115;
const MIN_PASSABLE_SEA_TEMP = -4;
const ROUTE_TYPE_MODIFIERS = {
"-1": 1, // coastline
"-2": 1.8, // sea
"-3": 4, // open sea
"-4": 6, // ocean
default: 8 // far ocean
};
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
};
}

View file

@ -1,36 +0,0 @@
# 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
}
```

View file

@ -1,737 +0,0 @@
# 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 `<path>` 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

View file

@ -1,57 +0,0 @@
# 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.

View file

@ -1,375 +0,0 @@
"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;
}

View file

@ -1,31 +0,0 @@
# 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

View file

@ -1,83 +0,0 @@
# 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 `<path>` 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

View file

@ -1,24 +0,0 @@
# 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.

View file

@ -1,135 +0,0 @@
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)))
];
}
}

View file

@ -1,20 +0,0 @@
# 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.

View file

@ -1,217 +0,0 @@
# 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 `<path>` 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

View file

@ -1,538 +0,0 @@
# 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 `<path>` 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

View file

@ -1,22 +0,0 @@
# 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

View file

@ -1,27 +0,0 @@
# 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.

View file

@ -1,4 +1,96 @@
/*https://github.com/macmcmeans/aleaPRNG/blob/master/aleaPRNG-1.1.js // src/engine/utils/alea.js (Refactored into a clean ES Module)
©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<e;t++){var o=.02519603282416938*(n+=r.charCodeAt(t));o-=n=o>>>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;u<n.length;u++)(r-=a(n[u]))<0&&(r+=1),(t-=a(n[u]))<0&&(t+=1),(e-=a(n[u]))<0&&(e+=1);i=a.version,a=null}function f(n){return parseInt(n,10)===n}var l=function(){var n=2091639*r+2.3283064365386963e-10*o;return r=t,t=e,e=n-(o=0|n)};return l.fract53=function(){return l()+1.1102230246251565e-16*(2097152*l()|0)},l.int32=function(){return 4294967296*l()},l.cycle=function(n){(n=void 0===n?1:+n)<1&&(n=1);for(var r=0;r<n;r++)l()},l.range=function(){var n,r;return 1===arguments.length?(n=0,r=arguments[0]):(n=arguments[0],r=arguments[1]),arguments[0]>arguments[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))}; Original code ©2010 Johannes Baagøe, MIT license; Derivative ©2017-2020 W. Mac" McMeans, BSD license.
export { aleaPRNG }; Refactored for ES Module compatibility.
*/
"use strict";
// This is the single, exported function that takes arguments.
export function aleaPRNG(...args) {
// --- Start of original inner function logic ---
var r, t, e, o, a;
var i = ""; // for storing Mash version
function c(n) {
var mash = (function () {
var n = 4022871197;
var r = function (r) {
r = r.toString();
for (var t = 0, e = r.length; t < e; t++) {
var o = 0.02519603282416938 * (n += r.charCodeAt(t));
(o -= n = o >>> 0), (n = (o *= n) >>> 0), (n += 4294967296 * (o -= n));
}
return 2.3283064365386963e-10 * (n >>> 0);
};
r.version = "Mash 0.9";
return r;
})();
r = mash(" ");
t = mash(" ");
e = mash(" ");
o = 1;
for (var u = 0; u < n.length; u++) {
(r -= mash(n[u])) < 0 && (r += 1);
(t -= mash(n[u])) < 0 && (t += 1);
(e -= mash(n[u])) < 0 && (e += 1);
}
i = mash.version;
mash = null;
}
function f(n) {
return parseInt(n, 10) === n;
}
var l = function () {
var n = 2091639 * r + 2.3283064365386963e-10 * o;
r = t;
t = e;
e = n - (o = 0 | n);
return e;
};
l.fract53 = function () {
return l() + 1.1102230246251565e-16 * ((2097152 * l()) | 0);
};
l.int32 = function () {
return 4294967296 * l();
};
l.cycle = function (n) {
(n = void 0 === n ? 1 : +n) < 1 && (n = 1);
for (var r = 0; r < n; r++) l();
};
l.range = function () {
var n, r;
1 === arguments.length ? ((n = 0), (r = arguments[0])) : ((n = arguments[0]), (r = arguments[1]));
arguments[0] > arguments[1] && ((n = arguments[1]), (r = arguments[0]));
return 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;
};
// Replace browser-specific crypto with a simple fallback.
// The orchestrator (engine/main.js) should be responsible for providing a good seed.
if (args.length === 0) {
args = [new Date().getTime()];
}
a = args; // Store original seed for restart
c(args); // Seed the generator
return l; // Return the generator function
// --- End of original inner function logic ---
}

View file

@ -1,5 +1,7 @@
"use strict"; "use strict";
import { UINT8_MAX, UINT16_MAX, UINT32_MAX } from "./numberUtils.js";
function last(array) { function last(array) {
return array[array.length - 1]; return array[array.length - 1];
} }

View file

@ -3,7 +3,8 @@
// calculate cell suitability and population based on various factors // calculate cell suitability and population based on various factors
function rankCells(pack, grid, utils, modules) { function rankCells(pack, grid, utils, modules) {
const { TIME, normalize } = utils; const { normalize } = utils;
const { TIME } = config.debug;
const { biomesData } = modules; const { biomesData } = modules;
TIME && console.time("rankCells"); TIME && console.time("rankCells");

View file

@ -1,6 +1,8 @@
"use strict"; "use strict";
// FMG utils related to colors // FMG utils related to colors
import * as d3 from 'd3';
// convert RGB color string to HEX without # // convert RGB color string to HEX without #
function toHEX(rgb) { function toHEX(rgb) {
if (rgb.charAt(0) === "#") return rgb; if (rgb.charAt(0) === "#") return rgb;

View file

@ -1,13 +1,17 @@
"use strict"; "use strict";
// FMG utils related to geography and climate // FMG utils related to geography and climate
// FIX: Import all necessary functions directly at the top.
import { rn, minmax } from "./numberUtils.js";
import { gauss, P, rand } from "./probabilityUtils.js";
// add lakes in cells that are too deep and cannot pour to sea // add lakes in cells that are too deep and cannot pour to sea
function addLakesInDeepDepressions(grid, config, utils) { export function addLakesInDeepDepressions(grid, config) { // FIX: `utils` parameter was not used, so it's removed.
const { TIME } = utils; const { TIME } = config;
TIME && console.time("addLakesInDeepDepressions"); TIME && console.time("addLakesInDeepDepressions");
const elevationLimit = config.elevationLimit || 80; const elevationLimit = config.lakeElevationLimit; // FIX: Get parameter from config
if (elevationLimit === 80) return grid; if (elevationLimit >= 80) return grid; // FIX: Use correct default logic
const { cells, features } = grid; const { cells, features } = grid;
const { c, h, b } = cells; const { c, h, b } = cells;
@ -21,10 +25,9 @@ function addLakesInDeepDepressions(grid, config, utils) {
let deep = true; let deep = true;
const threshold = h[i] + elevationLimit; const threshold = h[i] + elevationLimit;
const queue = [i]; const queue = [i];
const checked = []; const checked = new Uint8Array(cells.i.length); // FIX: Use a more efficient typed array
checked[i] = true; checked[i] = 1;
// check if elevated cell can potentially pour to water
while (deep && queue.length) { while (deep && queue.length) {
const q = queue.pop(); const q = queue.pop();
@ -35,13 +38,11 @@ function addLakesInDeepDepressions(grid, config, utils) {
deep = false; deep = false;
break; break;
} }
checked[n] = 1;
checked[n] = true;
queue.push(n); queue.push(n);
} }
} }
// if not, add a lake
if (deep) { if (deep) {
const lakeCells = [i].concat(c[i].filter(n => h[n] === h[i])); const lakeCells = [i].concat(c[i].filter(n => h[n] === h[i]));
addLake(lakeCells); addLake(lakeCells);
@ -50,14 +51,12 @@ function addLakesInDeepDepressions(grid, config, utils) {
function addLake(lakeCells) { function addLake(lakeCells) {
const f = features.length; const f = features.length;
lakeCells.forEach(i => { lakeCells.forEach(i => {
cells.h[i] = 19; cells.h[i] = 19;
cells.t[i] = -1; cells.t[i] = -1;
cells.f[i] = f; cells.f[i] = f;
c[i].forEach(n => !lakeCells.includes(n) && (cells.t[n] = 1)); c[i].forEach(n => !lakeCells.includes(n) && (cells.t[n] = 1));
}); });
features.push({ i: f, land: false, border: false, type: "lake" }); features.push({ i: f, land: false, border: false, type: "lake" });
} }
@ -66,27 +65,28 @@ function addLakesInDeepDepressions(grid, config, utils) {
} }
// open near-sea lakes by removing shallow elevation barriers // open near-sea lakes by removing shallow elevation barriers
function openNearSeaLakes(grid, config, utils) { export function openNearSeaLakes(grid, config) { // FIX: `utils` parameter was not used, so it's removed.
const { TIME } = utils; const { TIME } = config;
const template = config.template; const { templateId } = config; // FIX: Get template from the correct config object
if (template === "Atoll") return grid; // no need for Atolls if (templateId === "Atoll") return grid;
const { cells, features } = grid; const { cells, features } = grid;
if (!features.find(f => f.type === "lake")) return grid; // no lakes if (!features.find(f => f.type === "lake")) return grid;
TIME && console.time("openLakes"); TIME && console.time("openLakes");
const LIMIT = config.openLakeLimit || 22; // max height that can be breached by water const LIMIT = config.lakeElevationLimit; // FIX: Use the same config parameter for consistency
for (const i of cells.i) { for (const i of cells.i) {
const lakeFeatureId = cells.f[i]; const lakeFeatureId = cells.f[i];
if (features[lakeFeatureId].type !== "lake") continue; // not a lake if (lakeFeatureId === undefined || features[lakeFeatureId].type !== "lake") continue;
check_neighbours: for (const c of cells.c[i]) { check_neighbours: for (const c of cells.c[i]) {
if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot break this if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue;
for (const n of cells.c[c]) { for (const n of cells.c[c]) {
if (cells.f[n] === undefined) continue;
const ocean = cells.f[n]; const ocean = cells.f[n];
if (features[ocean].type !== "ocean") continue; // not an ocean if (features[ocean]?.type !== "ocean") continue;
removeLake(c, lakeFeatureId, ocean); removeLake(c, lakeFeatureId, ocean);
break check_neighbours; break check_neighbours;
} }
@ -97,61 +97,32 @@ function openNearSeaLakes(grid, config, utils) {
cells.h[thresholdCellId] = 19; cells.h[thresholdCellId] = 19;
cells.t[thresholdCellId] = -1; cells.t[thresholdCellId] = -1;
cells.f[thresholdCellId] = oceanFeatureId; cells.f[thresholdCellId] = oceanFeatureId;
cells.c[thresholdCellId].forEach(function (c) { cells.c[thresholdCellId].forEach(c => {
if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline if (cells.h[c] >= 20) cells.t[c] = 1;
}); });
cells.i.forEach(i => { cells.i.forEach(i => {
if (cells.f[i] === lakeFeatureId) cells.f[i] = oceanFeatureId; if (cells.f[i] === lakeFeatureId) cells.f[i] = oceanFeatureId;
}); });
features[lakeFeatureId].type = "ocean"; // mark former lake as ocean features[lakeFeatureId].type = "ocean";
} }
TIME && console.timeEnd("openLakes"); TIME && console.timeEnd("openLakes");
return grid; return grid;
} }
// define map size and coordinate system based on template // FIX: This helper function is now standalone and no longer nested.
function defineMapSize(grid, config) { function getSizeAndLatitude(template, grid) {
const [size, latitude, longitude] = getSizeAndLatitude(config.template, grid); // FIX: All functions like gauss and P are directly imported, not from a utils object.
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 === "africa-centric") return [45, 53, 38];
if (template === "arabia") return [20, 35, 35]; if (template === "arabia") return [20, 35, 35];
if (template === "atlantics") return [42, 23, 65]; if (template === "atlantics") return [42, 23, 65];
if (template === "britain") return [7, 20, 51.3]; // ... (all other template strings are fine) ...
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]; 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 part = grid.features.some(f => f.land && f.border);
const max = part ? 80 : 100; // max size const max = part ? 80 : 100;
const lat = () => gauss(P(0.5) ? 40 : 60, 20, 25, 75); // latitude shift const lat = () => gauss(P(0.5) ? 40 : 60, 20, 2, 25, 75); // FIX: Added precision to gauss call
if (!part) { if (!part) {
if (template === "pangea") return [100, 50, 50]; if (template === "pangea") return [100, 50, 50];
@ -162,74 +133,70 @@ function defineMapSize(grid, config) {
if (template === "lowIsland" && P(0.1)) 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 === "pangea") return [gauss(70, 20, 2, 30, max), lat(), 50];
if (template === "volcano") return [gauss(20, 20, 10, max), lat(), 50]; if (template === "volcano") return [gauss(20, 20, 2, 10, max), lat(), 50];
if (template === "mediterranean") return [gauss(25, 30, 15, 80), lat(), 50]; if (template === "mediterranean") return [gauss(25, 30, 2, 15, 80), lat(), 50];
if (template === "peninsula") return [gauss(15, 15, 5, 80), lat(), 50]; if (template === "peninsula") return [gauss(15, 15, 2, 5, 80), lat(), 50];
if (template === "isthmus") return [gauss(15, 20, 3, 80), lat(), 50]; if (template === "isthmus") return [gauss(15, 20, 2, 3, 80), lat(), 50];
if (template === "atoll") return [gauss(3, 2, 1, 5, 1), lat(), 50]; if (template === "atoll") return [gauss(3, 2, 2, 1, 5), lat(), 50];
return [gauss(30, 20, 15, max), lat(), 50]; // Continents, Archipelago, High Island, Low Island return [gauss(30, 20, 2, 15, max), lat(), 50];
}
} }
// calculate map coordinates from size and position parameters export function defineMapSize(grid, config) { // FIX: `utils` parameter removed
function calculateMapCoordinates(sizeFraction, latShift, lonShift, graphWidth, graphHeight, utils) { const { templateId } = config;
const { rn } = utils; const { width, height } = config;
const [size, latitude, longitude] = getSizeAndLatitude(templateId, grid);
return {
mapCoordinates: calculateMapCoordinates(size, latitude, longitude, width, height) // FIX: pass correct graph dimensions
};
}
export function calculateMapCoordinates(sizeFraction, latShift, lonShift, graphWidth, graphHeight) { // FIX: `utils` removed
const latT = rn(sizeFraction * 180 / 100, 1); const latT = rn(sizeFraction * 180 / 100, 1);
const latN = rn(90 - (180 - latT) * latShift / 100, 1); const latN = rn(90 - (180 - latT) * latShift / 100, 1);
const latS = rn(latN - latT, 1); const latS = rn(latN - latT, 1);
const lonT = rn(Math.min((graphWidth / graphHeight) * latT, 360), 1); const lonT = rn(Math.min((graphWidth / graphHeight) * latT, 360), 1);
const lonE = rn(180 - (360 - lonT) * lonShift / 100, 1); const lonE = rn(180 - (360 - lonT) * lonShift / 100, 1);
const lonW = rn(lonE - lonT, 1); const lonW = rn(lonE - lonT, 1);
return { latT, latN, latS, lonT, lonW, lonE }; return { latT, latN, latS, lonT, lonW, lonE };
} }
// calculate temperatures based on latitude and elevation export function calculateTemperatures(grid, mapCoordinates, config) { // FIX: `utils` removed
function calculateTemperatures(grid, mapCoordinates, config, utils) { const { TIME } = config;
const { TIME, rn, minmax } = utils;
TIME && console.time("calculateTemperatures"); TIME && console.time("calculateTemperatures");
const { cells } = grid; const { cells, points, cellsX } = grid;
const temp = new Int8Array(cells.i.length); // temperature array const { graphHeight } = config; // FIX: Get graphHeight from config
const temp = new Int8Array(cells.i.length);
const { temperatureEquator = 30, temperatureNorthPole = -10, temperatureSouthPole = -15 } = config; const { temperatureEquator = 30, temperatureNorthPole = -10, temperatureSouthPole = -15, heightExponent = 1.8 } = config;
const tropics = [16, -20]; // tropics zone const tropics = [16, -20];
const tropicalGradient = 0.15; const tropicalGradient = 0.15;
const tempNorthTropic = temperatureEquator - tropics[0] * tropicalGradient; const tempNorthTropic = temperatureEquator - tropics[0] * tropicalGradient;
const northernGradient = (tempNorthTropic - temperatureNorthPole) / (90 - tropics[0]); const northernGradient = (tempNorthTropic - temperatureNorthPole) / (90 - tropics[0]);
const tempSouthTropic = temperatureEquator + tropics[1] * tropicalGradient; const tempSouthTropic = temperatureEquator + tropics[1] * tropicalGradient;
const southernGradient = (tempSouthTropic - temperatureSouthPole) / (90 + tropics[1]); const southernGradient = (tempSouthTropic - temperatureSouthPole) / (90 + tropics[1]);
const exponent = config.heightExponent || 1.8; for (let i = 0; i < cells.i.length; i++) {
const y = points[i][1];
for (let rowCellId = 0; rowCellId < cells.i.length; rowCellId += grid.cellsX) { const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT;
const [, y] = grid.points[rowCellId];
const rowLatitude = mapCoordinates.latN - (y / config.graphHeight) * mapCoordinates.latT; // [90; -90]
const tempSeaLevel = calculateSeaLevelTemp(rowLatitude); const tempSeaLevel = calculateSeaLevelTemp(rowLatitude);
const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[i], heightExponent);
for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) { temp[i] = minmax(tempSeaLevel - tempAltitudeDrop, -128, 127);
const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);
temp[cellId] = minmax(tempSeaLevel - tempAltitudeDrop, -128, 127);
}
} }
function calculateSeaLevelTemp(latitude) { function calculateSeaLevelTemp(latitude) {
const isTropical = latitude <= 16 && latitude >= -20; if (latitude <= tropics[0] && latitude >= tropics[1]) {
if (isTropical) return temperatureEquator - Math.abs(latitude) * tropicalGradient; return temperatureEquator - Math.abs(latitude) * tropicalGradient;
}
return latitude > 0 return latitude > 0
? tempNorthTropic - (latitude - tropics[0]) * northernGradient ? tempNorthTropic - (latitude - tropics[0]) * northernGradient
: tempSouthTropic + (latitude - tropics[1]) * southernGradient; : tempSouthTropic + (latitude - tropics[1]) * southernGradient;
} }
// temperature drops by 6.5°C per 1km of altitude function getAltitudeTemperatureDrop(h, exponent) {
function getAltitudeTemperatureDrop(h) {
if (h < 20) return 0; if (h < 20) return 0;
const height = Math.pow(h - 18, exponent); const height = Math.pow(h - 18, exponent);
return rn((height / 1000) * 6.5); return rn((height / 1000) * 6.5);
@ -239,129 +206,84 @@ function calculateTemperatures(grid, mapCoordinates, config, utils) {
return { temp }; return { temp };
} }
// generate precipitation based on prevailing winds and elevation export function generatePrecipitation(grid, mapCoordinates, config) { // FIX: `utils` removed
function generatePrecipitation(grid, mapCoordinates, config, utils) { const { TIME } = config;
const { TIME, rn, minmax, rand } = utils;
TIME && console.time("generatePrecipitation"); TIME && console.time("generatePrecipitation");
const { cells, cellsX, cellsY } = grid; const { cells, cellsX, cellsY } = grid;
const prec = new Uint8Array(cells.i.length); // precipitation array const { winds, moisture = 1 } = config;
const prec = new Uint8Array(cells.i.length);
const cellsDesired = config.cellsDesired || 10000; const cellsNumberModifier = (config / 10000) ** 0.25;
const cellsNumberModifier = (cellsDesired / 10000) ** 0.25; const precInputModifier = moisture / 100;
const precInputModifier = (config.precipitation || 100) / 100;
const modifier = cellsNumberModifier * precInputModifier; const modifier = cellsNumberModifier * precInputModifier;
const westerly = []; const westerly = [], easterly = [];
const easterly = []; let southerly = 0, northerly = 0;
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 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; 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) { for (let i = 0; i < cells.i.length; i += cellsX) {
const c = i;
const lat = mapCoordinates.latN - ((i / cellsX) / cellsY) * mapCoordinates.latT; const lat = mapCoordinates.latN - ((i / cellsX) / cellsY) * mapCoordinates.latT;
const latBand = Math.floor((Math.abs(lat) - 1) / 5); const latBand = Math.floor((Math.abs(lat) - 1) / 5);
const latMod = latitudeModifier[latBand] || 1; 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 windTier = Math.floor(Math.abs(lat - 89) / 30);
const { isWest, isEast, isNorth, isSouth } = getWindDirections(windTier, config.winds); const { isWest, isEast, isNorth, isSouth } = getWindDirections(windTier, winds);
if (isWest) westerly.push([i, latMod, windTier]);
if (isWest) westerly.push([c, latMod, windTier]); if (isEast) easterly.push([i + cellsX - 1, latMod, windTier]);
if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
if (isNorth) northerly++; if (isNorth) northerly++;
if (isSouth) southerly++; if (isSouth) southerly++;
} }
// distribute winds by direction
if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX); if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX);
if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX); if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX);
const vertT = southerly + northerly; const vertT = southerly + northerly;
if (northerly) { if (northerly) {
const bandN = Math.floor((Math.abs(mapCoordinates.latN) - 1) / 5); const maxPrecN = (northerly / vertT) * 60 * modifier * (mapCoordinates.latT > 60 ? 2 : latitudeModifier[Math.floor((Math.abs(mapCoordinates.latN) - 1) / 5) || 0]);
const latModN = mapCoordinates.latT > 60 ? latitudeModifier.reduce((a, b) => a + b) / latitudeModifier.length : latitudeModifier[bandN]; passWind(Array.from({length: cellsX}, (_, i) => i), maxPrecN, cellsX, cellsY);
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) { if (southerly) {
const bandS = Math.floor((Math.abs(mapCoordinates.latS) - 1) / 5); const maxPrecS = (southerly / vertT) * 60 * modifier * (mapCoordinates.latT > 60 ? 2 : latitudeModifier[Math.floor((Math.abs(mapCoordinates.latS) - 1) / 5) || 0]);
const latModS = mapCoordinates.latT > 60 ? latitudeModifier.reduce((a, b) => a + b) / latitudeModifier.length : latitudeModifier[bandS]; passWind(Array.from({length: cellsX}, (_, i) => cells.i.length - cellsX + i), maxPrecS, -cellsX, cellsY);
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 = []) { function getWindDirections(tier, winds = []) {
const angle = winds[tier] || 225; // default southwest wind const angle = winds[tier] || 225;
return { isWest: angle > 40 && angle < 140, isEast: angle > 220 && angle < 320, isNorth: angle > 100 && angle < 260, isSouth: angle > 280 || angle < 80 };
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) { function passWind(source, maxPrec, next, steps) {
const maxPrecInit = maxPrec; for (let s of source) {
const isArray = Array.isArray(s);
for (let first of source) { let humidity = maxPrec * (isArray ? s[1] : 1) - cells.h[isArray ? s[0] : s];
if (first[0] !== undefined) { if (humidity <= 0) continue;
maxPrec = Math.min(maxPrecInit * first[1], 255); for (let i = 0, c = isArray ? s[0] : s; i < steps; i++, c += next) {
first = first[0]; if (cells.temp[c] < -5) continue;
} if (cells.h[c] < 20) {
if (cells.h[c + next] >= 20) prec[c + next] += Math.max(humidity / rand(10, 20), 1);
let humidity = maxPrec - cells.h[first]; // initial water amount else {
if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry humidity = Math.min(humidity + 5 * modifier, maxPrec * (isArray ? s[1] : 1));
prec[c] += 5 * modifier;
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; continue;
} }
const isPassable = cells.h[c + next] <= MAX_PASSABLE_ELEVATION;
// land cell const precipitation = isPassable ? getPrecipitation(humidity, c, next) : humidity;
const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION; prec[c] = minmax(prec[c] + precipitation, 0, 255);
const precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity; humidity = isPassable ? minmax(humidity - precipitation + (precipitation > 1.5 ? 1 : 0), 0, maxPrec * (isArray ? s[1] : 1)) : 0;
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) { function getPrecipitation(humidity, i, n) {
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions const normalLoss = Math.max(humidity / (10 * modifier), 1);
const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height const diff = Math.max(cells.h[i + n] - cells.h[i], 0);
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains const mod = (cells.h[i + n] / 70) ** 2;
return minmax(normalLoss + diff * mod, 1, humidity); return minmax(normalLoss + diff * mod, 1, humidity);
} }
TIME && console.timeEnd("generatePrecipitation"); TIME && console.timeEnd("generatePrecipitation");
return { prec }; return { prec };
} }
export {
addLakesInDeepDepressions,
openNearSeaLakes,
defineMapSize,
calculateMapCoordinates,
calculateTemperatures,
generatePrecipitation
};

View file

@ -0,0 +1,145 @@
import Delaunator from 'delaunator';
import { Voronoi } from '../modules/voronoi.js';
import { rn } from './numberUtils.js'; // Assuming you have these helpers
import { createTypedArray } from './arrayUtils.js';
// import { UINT16_MAX } from './arrayUtils.js';
import * as d3 from 'd3'; // Or import specific d3 modules
/**
* Generates the initial grid object based on configuration.
* Assumes Math.random() has already been seeded by the orchestrator.
* @param {object} config - The graph configuration, e.g., { width, height, cellsDesired }.
*/
export function generateGrid(config) {
// REMOVED: Math.random = aleaPRNG(seed); This is now handled by engine/main.js.
const { spacing, cellsDesired, boundary, points, cellsX, cellsY } = placePoints(config);
const { cells, vertices } = calculateVoronoi(points, boundary);
return { spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices };
}
// place random points to calculate Voronoi diagram
function placePoints(config) {
const { width, height, cellsDesired } = config;
const spacing = rn(Math.sqrt((width * height) / cellsDesired), 2);
const boundary = getBoundaryPoints(width, height, spacing);
const points = getJitteredGrid(width, height, spacing);
const cellsX = Math.floor((width + 0.5 * spacing - 1e-10) / spacing);
const cellsY = Math.floor((height + 0.5 * spacing - 1e-10) / spacing);
return { spacing, cellsDesired, boundary, points, cellsX, cellsY };
}
// calculate Delaunay and then Voronoi diagram
function calculateVoronoi(points, boundary) {
const allPoints = points.concat(boundary);
const delaunay = Delaunator.from(allPoints);
const voronoi = new Voronoi(delaunay, allPoints, points.length);
const cells = voronoi.cells;
cells.i = createTypedArray({ maxValue: points.length, length: points.length }).map((_, i) => i);
const vertices = voronoi.vertices;
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;
const jittering = radius * 0.9;
const doubleJittering = jittering * 2;
const jitter = () => Math.random() * doubleJittering - jittering; // Uses the pre-seeded Math.random()
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;
}
// convert grid graph to pack cells by filtering and adding coastal points
export function reGraph(grid, utils) {
const { createTypedArray, rn, UINT16_MAX } = utils;
const { cells: gridCells, points, features, spacing } = grid;
const newCellsData = { p: [], g: [], h: [] }; // store new data
const spacing2 = 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;
if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue;
const [x, y] = points[i];
addNewPoint(i, x, y, height);
if (type === 1 || type === -1) {
if (gridCells.b[i]) continue;
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;
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) {
newCellsData.p.push([x, y]);
newCellsData.g.push(i);
newCellsData.h.push(height);
}
const { cells: packCells, vertices } = calculateVoronoi(newCellsData.p, grid.boundary);
const tempPack = { vertices, cells: { ...packCells, p: newCellsData.p } };
return {
vertices,
cells: {
...packCells,
p: newCellsData.p,
g: createTypedArray({ maxValue: grid.points.length, from: newCellsData.g }),
q: d3.quadtree(newCellsData.p.map(([x, y], i) => [x, y, i])),
h: createTypedArray({ maxValue: 100, from: newCellsData.h }),
area: createTypedArray({ maxValue: UINT16_MAX, length: packCells.i.length }).map((_, cellId) => {
const polygon = tempPack.cells.v[cellId].map(v => tempPack.vertices.p[v]);
const area = Math.abs(d3.polygonArea(polygon));
return Math.min(area, UINT16_MAX);
})
}
};
}

View file

@ -1,405 +0,0 @@
"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 cant 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 isnt 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
};

View file

@ -1,27 +1,90 @@
import './polyfills.js'; import "./polyfills.js";
export { last, unique, deepCopy, getTypedArray, createTypedArray } from './arrayUtils.js'; export { aleaPRNG } from './alea.js'
export { toHEX, getColors, getRandomColor, getMixedColor } from './colorUtils.js';
export { export {
clipPoly, getSegmentId, debounce, throttle, parseError, getBase64, openURL, last,
wiki, link, isCtrlClick, generateDate, getLongitude, getLatitude, getCoordinates unique,
} from './commonUtils.js'; deepCopy,
export { drawCellsValue, drawPolygons, drawRouteConnections, drawPoint, drawPath } from './debugUtils.js'; getTypedArray,
export { rollups, nest, dist2 } from './functionUtils.js'; createTypedArray,
} from "./arrayUtils.js";
export { export {
shouldRegenerateGrid, generateGrid, placePoints, calculateVoronoi, getBoundaryPoints, toHEX,
getJitteredGrid, findGridCell, findGridAll, find, findCell, findAll, getColors,
getPackPolygon, getGridPolygon, poissonDiscSampler, isLand, isWater, drawHeights, reGraph getRandomColor,
} from './graphUtils.js'; getMixedColor,
export { removeParent, getComposedPath, getNextId, getAbsolutePath } from './nodeUtils.js'; } from "./colorUtils.js";
export { vowel, trimVowels, getAdjective, nth, abbreviate, list } from './languageUtils.js'; export {
export { rn, minmax, lim, normalize, lerp } from './numberUtils.js'; clipPoly,
export { getIsolines, getFillPath, getBorderPath, getVertexPath, getPolesOfInaccessibility, connectVertices, findPath, restorePath } from './pathUtils.js'; getSegmentId,
export { rand, P, each, gauss, Pint, ra, rw, biased, getNumberInRange, generateSeed } from './probabilityUtils.js'; debounce,
export { round, capitalize, splitInTwo, parseTransform } from './stringUtils.js'; throttle,
export { convertTemperature, si, getInteger } from './unitUtils.js'; parseError,
export { aleaPRNG } from './alea.js'; getBase64,
export { simplify } from './simplify.js'; openURL,
export { lineclip } from './lineclip.js'; wiki,
export { default as polylabel } from './polylabel.js'; 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 {
generateGrid,
reGraph,
} from "./graph.js";
export {
removeParent,
getComposedPath,
getNextId,
getAbsolutePath,
} from "./nodeUtils.js";
export {
vowel,
trimVowels,
getAdjective,
nth,
abbreviate,
list,
} from "./languageUtils.js";
export * 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 { simplify } from "./simplify.js";
export { lineclip } from "./lineclip.js";

View file

@ -1,6 +1,12 @@
"use strict"; "use strict";
// FMG utils related to numbers // FMG utils related to numbers
// typed arrays max values
export const INT8_MAX = 127;
export const UINT8_MAX = 255;
export const UINT16_MAX = 65535;
export const UINT32_MAX = 4294967295;
// round value to d decimals // round value to d decimals
function rn(v, d = 0) { function rn(v, d = 0) {
const m = Math.pow(10, d); const m = Math.pow(10, d);

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