more porting work

This commit is contained in:
barrulus 2025-08-05 13:12:07 -04:00
parent 37391c8e8b
commit 7f31969f50
38 changed files with 3673 additions and 463 deletions

6
.gitignore vendored
View file

@ -1,5 +1,7 @@
.vscode
.idea
.vscode/
.idea/
.claude/
.obsidian/
/node_modules
/dist
/coverage

112
procedural/CLAUDE.md Normal file
View file

@ -0,0 +1,112 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Common Development Commands
```bash
# Install dependencies
npm install
# Development server (Vite)
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Generate map via CLI (work in progress)
node cli.js --preset default --output map.json
node cli.js --config myconfig.json --seed myseed --output mymap.json
```
## High-Level Architecture
This project is being ported from a tightly-coupled browser application to a headless procedural generation engine with separate presentation layer.
### Project Structure
```
/procedural
├── src/
│ ├── engine/ # Headless map generation engine
│ │ ├── main.js # Orchestrator - exports generate(config) function
│ │ ├── modules/ # Generation modules (biomes, cultures, rivers, etc.)
│ │ └── utils/ # Environment-agnostic utilities
│ │
│ └── viewer/ # Web viewer application
│ ├── config-*.js # Configuration management
│ └── libs/ # Browser-specific libraries
├── cli.js # Command-line interface
├── main.js # Viewer entry point (Vite)
└── index.html # Web app HTML
```
### Core Architecture Flow
1. **Configuration**`generate(config)` → **MapData**
- Config object defines all generation parameters (see `src/viewer/config-schema.md`)
- Engine is pure JavaScript with no browser dependencies
- Returns serializable MapData object
2. **Module Pattern**
- Each module exports pure functions
- No global state manipulation
- Receives data, returns new data
- No IIFE wrappers or window dependencies
### Key Modules
- **Heightmap**: Generates terrain elevation
- **Features**: Marks geographic features (land, ocean, lakes)
- **Rivers**: Generates river systems
- **Biomes**: Assigns biomes based on climate
- **Cultures**: Places and expands cultures
- **BurgsAndStates**: Generates settlements and political entities
- **Routes**: Creates trade and travel routes
## Configuration System
Configuration drives the entire generation process. See `src/viewer/config-schema.md` for complete TypeScript interface.
Key sections:
- `graph`: Canvas dimensions and cell count
- `heightmap`: Terrain template selection
- `cultures`: Number and type of cultures
- `burgs`: States and settlements
- `debug`: Logging flags (TIME, WARN, INFO)
## Important Development Notes
1. **Ongoing Port**: Moving from DOM/SVG manipulation to pure data generation per `PORT_PLAN.md`
2. **Module Refactoring Pattern**:
```javascript
// OLD: window.Module = (function() { ... })();
// NEW: export function generateModule(data, config, utils) { ... }
```
3. **No Browser Dependencies in Engine**:
- No `window`, `document`, or DOM access
- No direct SVG/D3 manipulation
- All rendering logic stays in viewer
4. **Utility Organization**:
- Generic utilities in `src/engine/utils/`
- Specialized utilities imported as needed
- PRNG (Alea) for reproducible generation
5. **Data Flow**:
- Grid (coarse Voronoi mesh) → Pack (refined mesh)
- Sequential module execution builds up complete map
- Each module adds its data to the growing structure
## Testing Approach
Currently no formal test suite. When adding tests:
- Focus on engine modules (pure functions)
- Test with known seeds for reproducibility
- Validate output data structures

View file

@ -1,194 +0,0 @@
Excellent. The architectural backbone is now in place. We've successfully separated the *intent* (clicking "Generate" in the viewer) from the *execution* (running the pipeline in the engine).
Now we address the errors you're seeing. These errors are expected and are our roadmap for the final stage of the engine refactoring. They are caused by functions in your new `engine/main.js` that were originally defined in the massive, global `main.js` (/Users/barrulus/Fantasy-Map-Generator/main.js) file.
Our next task is to systematically move these remaining functions from `main.js` into their correct homes within the new engine structure, turning them into pure, importable modules.
### The Strategy: Categorize and Relocate
We will categorize the functions from `main.js` into logical groups and create new files for them, mostly within the `engine/utils/` directory, as they are largely helper functions for the main generation modules.
Here is the breakdown and your next set of instructions.
---
### Step 1: Create a Home for Graph Utilities
Many functions in `main.js` are related to the creation and manipulation of the Voronoi graph (`grid` and `pack`). The `graphUtils.js` you ported only contains a few of them. We need to create a more comprehensive module.
**Your Action:**
1. Create a new file: `src/engine/utils/graph.js`.
2. Find the following functions in the **original `main.js` (/Users/barrulus/Fantasy-Map-Generator/main.js) file**, cut them out, and paste them into your new `graph.js` file.
* `generateGrid()`
* `placePoints()`
* `calculateVoronoi()`
* `getBoundaryPoints()`
* `getJitteredGrid()`
* `reGraph()`
3. Refactor these functions to be pure ES modules:
* Add `export` before each function declaration.
* Remove all dependencies on global variables (`seed`, `graphWidth`, `graphHeight`, `grid`, `pack`, etc.). Pass them in as arguments.
* Import any necessary dependencies (like `Delaunator`, `Voronoi`, and other utils).
* Ensure they return their results instead of mutating global state.
**Example for `generateGrid`:**
```javascript
// src/engine/utils/graph.js
import { aleaPRNG } from './probability.js'; // Assuming this is where it will live
import { Delaunator } from '../../libs/delaunator.js';
import { Voronoi } from '../modules/voronoi.js';
// Takes config, returns a new grid object
export function generateGrid(config) {
const { seed, graphWidth, graphHeight } = config;
Math.random = aleaPRNG(seed);
const { spacing, cellsDesired, boundary, points, cellsX, cellsY } = placePoints(config);
const { cells, vertices } = calculateVoronoi(points, boundary);
return { spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed };
}
// placePoints needs config for graphWidth/Height and cellsDesired
function placePoints(config) {
// ... logic ...
return { spacing, cellsDesired, boundary, points, cellsX, cellsY };
}
// ... and so on for the other functions
```
---
### Step 2: Create a Home for Geographic and Climate Utilities
These functions deal with the physical properties of the map, like temperature and coordinates.
**Your Action:**
1. Create a new file: `src/engine/utils/geography.js`.
2. Find the following functions in the **original `main.txt` file**, cut them out, and paste them into your new file.
* `defineMapSize()`
* `calculateMapCoordinates()`
* `calculateTemperatures()`
* `generatePrecipitation()`
* `addLakesInDeepDepressions()`
* `openNearSeaLakes()`
3. Refactor them:
* Add `export` to each function.
* Inject dependencies (`grid`, `pack`, `options`, `config`, other utils).
* Return new or modified data structures instead of mutating globals.
**Example for `calculateTemperatures`:**
```javascript
// src/engine/utils/geography.js
// It needs the grid, mapCoordinates, and temperature options from the config
export function calculateTemperatures(grid, mapCoordinates, config) {
const cells = grid.cells;
const temp = new Int8Array(cells.i.length);
// ... existing logic ...
// ... use config.temperatureEquator etc. instead of options. ...
// for-loop to populate the `temp` array
// Return the new data
return { temp };
}
```
---
### Step 3: Create a Home for Population and Cell Ranking Utilities
This is a key part of the generation logic that determines where cultures and burgs can settle.
**Your Action:**
1. Create a new file: `src/engine/utils/cell.js`.
2. Find the `rankCells()` function in the **original `main.txt` file**, cut it out, and paste it into your new file.
3. Refactor it:
* `export function rankCells(pack, grid, utils, modules)`
* It will need dependencies like `pack`, `grid`, `utils.d3`, and `modules.biomesData`.
* It should return an object containing the new `s` (suitability) and `pop` (population) arrays.
* `return { s: newSuitabilityArray, pop: newPopulationArray };`
---
### Step 4: Update the Engine Orchestrator (`engine/main.js`)
Now that you've moved all these functions into modules, you need to update the orchestrator to import and use them correctly.
**Your Action:** Modify `src/engine/main.js` to look like this.
```javascript
// src/engine/main.js
// ... (existing module imports)
// Import the new utility modules
import * as Graph from "./utils/graph.js";
import * as Geography from "./utils/geography.js";
import * as Cell from "./utils/cell.js";
import * as Utils from "./utils/index.js";
export function generate(config) {
const timeStart = performance.now();
const { TIME, WARN, INFO } = Utils;
const seed = config.seed || Utils.generateSeed();
Math.random = Utils.aleaPRNG(seed);
INFO && console.group("Generating Map with Seed: " + seed);
// --- Grid Generation ---
let grid = Graph.generateGrid(config.graph);
grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils);
grid = Features.markupGrid(grid, config, Utils);
const { mapCoordinates } = Geography.defineMapSize(grid, config.map); // Now returns the coordinates object
grid = Geography.addLakesInDeepDepressions(grid, config.lakes, Utils);
grid = Geography.openNearSeaLakes(grid, config.lakes, Utils);
// --- Core Data Calculation ---
const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils);
grid.cells.temp = temp;
const { prec } = Geography.generatePrecipitation(grid, mapCoordinates, config.precipitation, Utils);
grid.cells.prec = prec;
// --- Pack Generation ---
let pack = Graph.reGraph(grid, Utils);
pack = Features.markupPack(pack, config, Utils, { Lakes });
// --- River Generation ---
const riverResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names });
pack = riverResult.pack;
// --- Biome and Population ---
const { biome } = Biomes.define(pack, grid, config.biomes, Utils);
pack.cells.biome = biome;
const { s, pop } = Cell.rankCells(pack, Utils, { biomesData: Biomes.getDefault() });
pack.cells.s = s;
pack.cells.pop = pop;
// --- Cultures, States, Burgs etc. (as before) ---
// ...
WARN && console.warn(`TOTAL GENERATION TIME: ${Utils.rn((performance.now() - timeStart) / 1000, 2)}s`);
INFO && console.groupEnd("Generated Map " + seed);
return { seed, grid, pack, mapCoordinates };
}
```
**Note:** You will also need to update your `viewer/main.js` `buildConfigFromUI` function to create nested config objects for the new parameters (e.g., `config.graph`, `config.heightmap`, `config.temperature`). Use your existing `_config.md` located in /Users/barrulus/Fantasy-Map-Generator/procedural/src/engine/support/*_config.md files as a guide.
### Your Goal for This Phase
Your goal is to have a fully functional `engine/main.js` that can execute the entire generation pipeline without relying on *any* functions from the old `main.js`. After this step, `main.js` should be almost empty, containing only UI-specific logic (like event handlers, drawing functions, etc.), which we will deal with later.
This is a significant undertaking. Take it one function at a time. The process is the same for each one:
1. **Move** the function to its new home.
2. **Export** it.
3. **Identify** its dependencies.
4. **Add** those dependencies to its argument list.
5. **Return** its result instead of mutating globals.
6. **Update** the caller (`engine/main.js`) to import and use the refactored function correctly.
Report back when you have completed this. We will then be ready to connect the engine's output to the rendering system.

View file

@ -0,0 +1,246 @@
# Call Pattern Issues in Engine Refactoring
## Overview
During the refactoring of the Fantasy Map Generator from a browser-dependent application to a headless engine, we've encountered systematic issues with how modules are called and how they access their dependencies. This document catalogues these patterns and provides a systematic approach to identifying and fixing them.
## The Problem
The original codebase used global variables and browser-specific APIs. The refactored engine uses dependency injection, but there are mismatches between:
1. **Function signatures** - What parameters functions expect
2. **Function calls** - What parameters are actually passed
3. **Data access patterns** - How modules access configuration and utilities
## Common Anti-Patterns Found
### 1. Config Nesting Mismatch
**Problem**: Modules expect config properties at the root level, but they're nested under specific sections.
**Example**:
```javascript
// ❌ Module expects:
config.culturesInput
config.culturesInSetNumber
// ✅ But config actually has:
config.cultures.culturesInput
config.cultures.culturesInSetNumber
```
**Pattern**: `config.{property}` vs `config.{section}.{property}`
**Files affected**: `cultures-generator.js`, `biomes.js`, `river-generator.js`
### 2. Missing Config Parameter
**Problem**: Modules expect full `config` object but are passed only a subsection.
**Example**:
```javascript
// ❌ Incorrect call:
Biomes.define(pack, grid, config.biomes, Utils)
// ✅ Correct call (module needs config.debug):
Biomes.define(pack, grid, config, Utils)
```
**Pattern**: Modules need `config.debug` but receive `config.{section}`
**Files affected**: `biomes.js`, `river-generator.js`
### 3. Missing Module Dependencies
**Problem**: Function signature doesn't include `modules` parameter but code tries to access module dependencies.
**Example**:
```javascript
// ❌ Function signature:
function generate(pack, grid, config, utils) {
// Code tries to use Names module
utils.Names.getNameBases() // ❌ Names not in utils
}
// ✅ Correct signature:
function generate(pack, grid, config, utils, modules) {
const { Names } = modules;
Names.getNameBases() // ✅ Correct access
}
```
**Files affected**: `cultures-generator.js`
### 4. Missing Parameter Propagation
**Problem**: Functions call other functions without passing required parameters.
**Example**:
```javascript
// ❌ Missing parameters:
Lakes.defineClimateData(h)
// ✅ Should pass all required params:
Lakes.defineClimateData(pack, grid, h, config, utils)
```
**Files affected**: `river-generator.js`, `features.js`
### 5. Global Variable References
**Problem**: Functions reference global variables that don't exist in headless environment.
**Example**:
```javascript
// ❌ References undefined globals:
function clipPoly(points, secure = 0) {
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
// ^^^^^^^^^^^ ^^^^^^^^^^^^ undefined
}
// ✅ Get from config:
function clipPoly(points, config, secure = 0) {
const graphWidth = config.graph.width || 1000;
const graphHeight = config.graph.height || 1000;
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}
```
**Files affected**: `commonUtils.js`
### 6. Context-Aware Wrappers
**Problem**: Utility functions expect parameters that aren't available in calling context.
**Example**:
```javascript
// ❌ isLand expects pack but called without it:
neighbors[cellId].filter(isLand)
// ✅ Create context-aware wrapper or pass explicitly:
neighbors[cellId].filter(i => isLand(i, pack))
```
**Files affected**: `features.js`
## Systematic Detection Strategy
### 1. Function Signature Analysis
For each exported function, check:
```bash
# Find function exports
grep -n "export.*function\|export const.*=" src/engine/modules/*.js
# Check what parameters they expect vs receive
```
### 2. Config Access Pattern Audit
```bash
# Find config property access
grep -rn "config\." src/engine/modules/ | grep -v "config\.debug"
# Check if properties exist in config structure
```
### 3. Module Dependency Check
```bash
# Find modules object usage
grep -rn "modules\." src/engine/modules/
# Find utils object access to modules
grep -rn "utils\.[A-Z]" src/engine/modules/
```
### 4. Global Reference Detection
```bash
# Find potential global references
grep -rn "\b[A-Z_][A-Z_]*\b" src/engine/ | grep -v "import\|export\|const\|let\|var"
```
### 5. Function Call Parameter Mismatch
```bash
# Find function calls and compare with signatures
grep -rn "\.generate\|\.define\|\.markup" src/engine/main.js
```
## Systematic Fix Pattern
### Step 1: Audit Function Signatures
1. List all exported functions in modules
2. Document expected parameters
3. Check all call sites
4. Identify mismatches
### Step 2: Config Structure Mapping
1. Document actual config structure from `config-builder.js`
2. Find all `config.{property}` accesses in modules
3. Map correct paths (`config.section.property`)
### Step 3: Dependency Injection Fix
1. Ensure all functions receive required parameters
2. Add `modules` parameter where needed
3. Update all call sites to pass correct parameters
### Step 4: Global Reference Elimination
1. Find all global variable references
2. Determine correct source (config, utils, passed parameters)
3. Update function signatures if needed
## Files Requiring Systematic Review
### High Priority (Core Generation Flow)
- `src/engine/main.js` - All module calls
- `src/engine/modules/biomes.js`
- `src/engine/modules/cultures-generator.js`
- `src/engine/modules/river-generator.js`
- `src/engine/modules/burgs-and-states.js`
- `src/engine/modules/features.js`
### Medium Priority (Utilities)
- `src/engine/utils/commonUtils.js`
- `src/engine/utils/cell.js`
- `src/engine/modules/lakes.js`
### Low Priority (Supporting Modules)
- `src/engine/modules/provinces-generator.js`
- `src/engine/modules/religions-generator.js`
- `src/engine/modules/military-generator.js`
- All other utility modules
## Verification Checklist
For each module function:
- [ ] Function signature matches all call sites
- [ ] All required parameters are passed
- [ ] Config properties accessed via correct path
- [ ] No global variable references
- [ ] Module dependencies properly injected
- [ ] Error handling for missing dependencies
## Example Systematic Fix
```javascript
// 1. Document current signature
function someModule(pack, config, utils) { ... }
// 2. Document all call sites
someModule(pack, config.section, utils) // ❌ Wrong config
someModule(pack, grid, config, utils) // ❌ Missing grid param
// 3. Determine correct signature
function someModule(pack, grid, config, utils, modules) { ... }
// 4. Update all call sites
someModule(pack, grid, config, utils, modules) // ✅ Correct
// 5. Update internal property access
// config.property → config.section.property
// utils.Module → modules.Module
```
This systematic approach will help identify and fix all parameter passing issues before they cause runtime errors.

View file

@ -0,0 +1,242 @@
# Data Mismatch Task Activity Log
This file logs all completed activities for fixing data mismatches in the Fantasy Map Generator.
## Task 1: Add Property Checks to All Modules
**Status**: Completed
**Date**: 2025-08-05
### Files Modified:
#### 1. src/engine/modules/heightmap-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `graph` object and `graph.cells` structure
- Check for `config.heightmap.templateId`
- Check for `config.debug` section
#### 2. src/engine/modules/lakes.js
**Changes made**: Added property validation checks at the start of the `detectCloseLakes` function
- Check for `pack.cells` and `pack.features` structures
- Check for `pack.cells.c` (neighbors) and `pack.cells.f` (features)
- Check for `heights` array
#### 3. src/engine/modules/burgs-and-states.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.culture` from Cultures module
- Check for `pack.cells.s` (suitability) from Cell ranking
- Check for `pack.cultures` from Cultures module
- Check for `config.statesNumber`
#### 4. src/engine/modules/cultures-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.s` (suitability) from Cell ranking
- Check for `config.culturesInput` and `config.culturesInSetNumber`
- Check for `config.debug` section
#### 5. src/engine/modules/biomes.js
**Changes made**: Added property validation checks at the start of the `define` function
- Check for `pack.cells.h` (heights) from heightmap processing
- Check for `grid.cells.temp` and `grid.cells.prec` from geography module
- Check for `pack.cells.g` (grid reference) from pack generation
- Check for `config.debug` section
#### 6. src/engine/modules/features.js
**Changes made**: Added property validation checks to two functions:
- `markupGrid` function: Check for `grid.cells.h` (heights), `grid.cells.c` (neighbors), and `config.debug`
- `markupPack` function: Check for `pack.cells.h` (heights), `pack.cells.c` (neighbors), and `grid.features`
#### 7. src/engine/modules/river-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.h` (heights) from heightmap processing
- Check for `pack.cells.t` (distance field) from features module
- Check for `pack.features` from features module
- Check for `modules.Lakes` dependency
- Check for `config.debug` section
#### 8. src/engine/modules/religions-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.culture` from Cultures module
- Check for `pack.cells.state` from BurgsAndStates module
- Check for `pack.cultures` from Cultures module
- Check for `config.religionsNumber`
- Check for `config.debug` section
#### 9. src/engine/modules/provinces-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.state` from BurgsAndStates module
- Check for `pack.cells.burg` from BurgsAndStates module
- Check for `pack.states` from BurgsAndStates module
- Check for `pack.burgs` from BurgsAndStates module
- Check for `config.debug` section
#### 10. src/engine/modules/routes-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.burg` from BurgsAndStates module
- Check for `pack.burgs` from BurgsAndStates module
- Check for `pack.cells.h` (heights) from heightmap processing
- Check for `pack.cells.t` (distance field) from features module
#### 11. src/engine/modules/military-generator.js
**Changes made**: Added property validation checks at the start of the `generate` function
- Check for `pack.cells.state` from BurgsAndStates module
- Check for `pack.states` from BurgsAndStates module
- Check for `pack.burgs` from BurgsAndStates module
- Check for `config.debug` section
### Summary:
Added property validation checks to 11 critical engine modules. Each module now validates required dependencies and configuration sections at startup, providing clear error messages when properties are missing. This implements Fix 1 from the Data_Mismatch_Tasks.md plan - adding simple property checks to fail fast with helpful error messages.
All checks follow the pattern:
```javascript
// Check required properties exist
if (!requiredProperty) {
throw new Error("ModuleName requires requiredProperty from DependencyModule");
}
```
This ensures clear dependency tracking and early error detection when modules are called with missing prerequisites.
## Task 2: Update Config Validator for Missing Fields
**Status**: Completed
**Date**: 2025-08-05
### Files Modified:
#### 1. src/viewer/config-validator.js
**Changes made**: Added simple required fields validation as specified in the task
**Functions added**:
- `validateRequiredFields(config, result)` - Validates specific required fields for modules
- `getCultureSetMax(culturesSet)` - Helper function to get maximum cultures for culture sets
**Required fields validated**:
- `cultures.culturesInSetNumber` - Validates based on culturesSet maximum
- `rivers.cellsCount` - Validates against graph.cellsDesired or defaults to 10000
**Implementation**: Added simple check for missing fields with warnings that show what the default values would be.
### Summary:
Updated the existing config validator to implement **Fix 2** from the Data_Mismatch_Tasks.md plan by adding the specific required fields validation as shown in the task example. The validator now checks for missing `cultures.culturesInSetNumber` and `rivers.cellsCount` fields and provides warnings when they are missing.
## Task 3: Update Documentation with Property Timeline
**Status**: Completed
**Date**: 2025-08-05
### Files Modified:
#### 1. docs/FMG Data Model.md
**Changes made**: Added comprehensive Property Availability Timeline section as specified in the task
**New section added**: "Property Availability Timeline"
- **Grid Properties section**: Documents when each grid property becomes available during generation
- **Pack Properties section**: Documents when each pack property becomes available during generation
- **Module Execution Flow**: Added mermaid flowchart diagram showing complete module execution sequence
**Properties documented**:
- Grid properties: `cells.h`, `cells.f`, `cells.t`, `cells.temp`, `cells.prec`
- Pack properties: `cells.h`, `cells.f`, `cells.t`, `cells.fl`, `cells.r`, `cells.biome`, `cells.s`, `cells.pop`, `cells.culture`, `cells.burg`, `cells.state`, `cells.religion`, `cells.province`
**Mermaid diagram**: Visual flowchart showing the complete generation pipeline from initial grid through all modules to final map data, with annotations showing what properties each module adds.
### Summary:
Implemented **Fix 3** from the Data_Mismatch_Tasks.md plan by adding the Property Availability Timeline section to the existing documentation. This addresses the "Pack/Grid structure differences" issue by clearly documenting when each property becomes available during the generation process.
The documentation now provides:
1. **Clear reference for developers** - Shows exactly when each property is available
2. **Module dependency tracking** - Visual flow shows which modules depend on others
3. **Pack vs Grid clarification** - Distinguishes between grid (coarse mesh) and pack (refined mesh) properties
4. **Complete generation pipeline** - Mermaid diagram shows the full execution flow from main.js
This helps developers understand data availability and prevents undefined reference errors by showing the exact timeline of when properties are added to the data structures.
## Task 4: Add Requirement Comments to All Modules
**Status**: Completed
**Date**: 2025-08-05
### Files Modified:
#### 1. src/engine/modules/heightmap-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: graph.cells, config.heightmap.templateId, config.debug
- **PROVIDES**: grid.cells.h (height values)
#### 2. src/engine/modules/lakes.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `detectCloseLakes` function
- **REQUIRES**: pack.cells, pack.features, pack.cells.c, pack.cells.f, heights array
- **PROVIDES**: Updated pack.features with closed property
#### 3. src/engine/modules/features.js
**Changes made**: Added JSDoc-style requirement comment blocks to both main functions
- **markupGrid REQUIRES**: grid.cells.h, grid.cells.c, config.debug
- **markupGrid PROVIDES**: grid.cells.f, grid.cells.t, grid.features
- **markupPack REQUIRES**: pack.cells.h, pack.cells.c, grid.features
- **markupPack PROVIDES**: pack.cells.f, pack.cells.t, pack.features
#### 4. src/engine/modules/biomes.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `define` function
- **REQUIRES**: pack.cells.h, grid.cells.temp, grid.cells.prec, pack.cells.g, config.debug
- **PROVIDES**: pack.cells.biome
#### 5. src/engine/modules/cultures-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.s, config.culturesInput, config.culturesInSetNumber
- **PROVIDES**: pack.cells.culture, pack.cultures
#### 6. src/engine/modules/burgs-and-states.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.culture, pack.cells.s, pack.cultures, config.statesNumber
- **PROVIDES**: pack.burgs, pack.states, pack.cells.burg, pack.cells.state
#### 7. src/engine/modules/river-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.h, pack.cells.t, pack.features, modules.Lakes, config.debug
- **PROVIDES**: pack.cells.fl, pack.cells.r, pack.cells.conf
#### 8. src/engine/modules/religions-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.culture, pack.cells.state, pack.cultures, config.religionsNumber, config.debug
- **PROVIDES**: pack.cells.religion, pack.religions
#### 9. src/engine/modules/provinces-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.state, pack.cells.burg, pack.states, pack.burgs, config.debug
- **PROVIDES**: pack.cells.province, pack.provinces
#### 10. src/engine/modules/routes-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.burg, pack.burgs, pack.cells.h, pack.cells.t
- **PROVIDES**: pack.routes, pack.cells.routes
#### 11. src/engine/modules/military-generator.js
**Changes made**: Added JSDoc-style requirement comment block at the top of the `generate` function
- **REQUIRES**: pack.cells.state, pack.states, pack.burgs, config.debug
- **PROVIDES**: pack.states[].military
### Summary:
Implemented **Fix 4** from the Data_Mismatch_Tasks.md plan by adding requirement comments to all 11 major engine modules. Each module now has clear JSDoc-style documentation at the top of its main function showing:
1. **Self-documenting modules** - Each module clearly states what it requires and provides
2. **No runtime overhead** - Comments are compile-time only and don't affect performance
3. **Clear for developers** - Easy to understand dependencies at a glance
4. **Dependency tracking** - Shows exact relationships between modules
The comment format follows the task specification:
```javascript
/**
* Module description
*
* REQUIRES:
* - dependency1 (from source module)
* - dependency2 (from source module)
*
* PROVIDES:
* - output1 (what this module adds)
* - output2 (what this module adds)
*/
```
This addresses the "Module dependencies" issue by making all dependencies explicit and self-documenting, complementing the runtime property checks from Task 1.

View file

@ -0,0 +1,177 @@
# Simplified Plan to Fix Data Mismatches in Fantasy Map Generator
This document outlines a practical plan to address the four common data mismatches identified in the MAP_GENERATION_TRACE.md:
1. **Modules expecting properties that don't exist yet**
2. **Config sections missing expected fields**
3. **Pack/Grid structure differences**
4. **Module dependencies**
## Problem Analysis
### 1. Modules Expecting Properties That Don't Exist Yet
**Current Issue**: Modules access properties like `cells.culture` before the Cultures module has run, causing undefined references and potential crashes.
**Root Cause**: Modules don't check if required properties exist before accessing them.
### 2. Config Sections Missing Expected Fields
**Current Issue**: Modules expect certain configuration fields that may not be present, even after validation.
**Root Cause**: The existing config validator may not catch all missing fields that modules expect.
### 3. Pack/Grid Structure Differences
**Current Issue**: Pack is a refined version of grid, but modules sometimes confuse which structure they're working with.
**Root Cause**: Similar naming and structure between pack and grid, but different levels of detail and available properties.
### 4. Module Dependencies
**Current Issue**: Some modules require data from other modules (e.g., Rivers needs Lakes, Cultures needs Names) but these dependencies aren't formally tracked.
**Root Cause**: No explicit dependency management system; modules assume previous modules have run successfully.
## Fix Plan
### Fix 1: Add Simple Property Checks to Each Module
Instead of complex wrappers or validators, add simple checks at the start of each module:
**Example for burgs-and-states.js:**
```javascript
export const generate = (pack, grid, config, utils) => {
// Check required properties exist
if (!pack.cells.culture) {
throw new Error("BurgsAndStates module requires cells.culture from Cultures module");
}
if (!pack.cells.s) {
throw new Error("BurgsAndStates module requires cells.s (suitability) from Cell ranking");
}
// Continue with existing code...
}
```
**Benefits:**
- Clear error messages
- No complex infrastructure
- Easy to add to existing modules
- Fail fast with helpful information
### Fix 2: Enhance Existing Config Validator
Update the existing `src/viewer/config-validator.js` to ensure all required fields are present:
```javascript
// Add to existing validator
const requiredFields = {
'cultures.culturesInSetNumber': (config) => {
// Ensure this field exists based on culturesSet
const maxCultures = getCultureSetMax(config.cultures.culturesSet);
return maxCultures;
},
'rivers.cellsCount': (config) => {
// Ensure this matches the actual cell count
return config.graph.cellsDesired || 10000;
}
};
```
**Benefits:**
- Uses existing infrastructure
- No duplication
- Config is complete before engine runs
### Fix 3: Document Property Timeline in Existing Docs
Add a section to the existing `docs/FMG Data Model.md`:
```markdown
## Property Availability Timeline
Properties are added to grid/pack progressively:
### Grid Properties (coarse mesh ~10K cells)
- `cells.h` - Available after: heightmap module
- `cells.t` - Available after: features module
- `cells.temp` - Available after: geography module
- `cells.prec` - Available after: geography module
### Pack Properties (refined mesh)
- `cells.biome` - Available after: biomes module
- `cells.s` - Available after: cell ranking
- `cells.pop` - Available after: cell ranking
- `cells.culture` - Available after: cultures module
- `cells.state` - Available after: states module
- `cells.burg` - Available after: burgs module
- `cells.religion` - Available after: religions module
- `cells.province` - Available after: provinces module
```
+ Add mermaid flow diagram
**Benefits:**
- Uses existing documentation
- Clear reference for developers
- No new files or folders
### Fix 4: Add Module Requirements as Comments
At the top of each module, clearly document what it requires:
```javascript
// src/engine/modules/burgs-and-states.js
"use strict";
/**
* Generates burgs (settlements) and states (political entities)
*
* REQUIRES:
* - pack.cells.culture (from cultures module)
* - pack.cells.s (from cell ranking)
* - pack.cultures (from cultures module)
*
* PROVIDES:
* - pack.burgs
* - pack.states
* - pack.cells.burg
* - pack.cells.state
*/
export const generate = (pack, grid, config, utils) => {
// ... module code
}
```
**Benefits:**
- Self-documenting
- No runtime overhead
- Clear for developers
### Optional: Debug Mode for Dependency Checking
Add to `src/engine/main.js`:
```javascript
// Only if debug flag is set
if (config.debug.CHECK_DEPENDENCIES) {
// Simple property existence checks before each module
if (!pack.cells.culture && moduleNeedsCulture) {
console.error("Missing required property: cells.culture");
}
}
```
## Notes for tasks.
Please log all completed activities in docs/Data_Mismatch_Task_Activity.md.
For each task, log the files you altered witrh the changes made in that file.
## Implementation Steps
1. **Task 1**: Add property checks to all modules
2. **Task 2**: Update config validator for missing fields
3. **Task 3**: Update documentation with property timeline, including a mermaid flow diagram
4. **Task 4**: Add requirement comments to all modules

View file

@ -0,0 +1,366 @@
**FMG data model** is poorly defined, inconsistent and not documented in the codebase. This page is the first and the only attempt to document it. Once everything is documented, it can be used for building a new consistent model.
FMG exposes all its data into the global namespace. The global namespace is getting polluted and it can cause conflicts with 3rd party extensions. Meanwhile it simplifies debugging and allows users to run custom JS code in dev console to alter the tool behavior.
# Basic objects
FMG has two meta-objects storing most of the map data:
* `grid` contains map data before _repacking_
* `pack` contains map data after _repacking_
Repacking is a process of amending an initial [voronoi diagram](https://en.wikipedia.org/wiki/Voronoi_diagram), that is based on a jittered square grid of points, into a voronoi diagram optimized for the current landmass (see [my old blog post](https://azgaar.wordpress.com/2017/10/05/templates) for the details). So the `pack` object is used for most of the data, but data optimized for square grid is available only via the `grid` object.
## Voronoi data
Both `grid` and `pack` objects include data representing voronoi diagrams and their inner connections. Both initial and repacked voronoi can be build from the initial set of points, so this data is stored in memory only. It does not included into the .map file and getting calculated on map load.
### Grid object
* `grid.cellsDesired`: `number` - initial count of cells/points requested for map creation. Used to define `spacing` and place points on a jittered square grid, hence the object name. Actual number of cells is defined by the number points able to place on a square grid. Default `cellsDesired` is 10 000, maximum - 100 000, minimal - 1 000
* `grid.spacing`: `number` - spacing between points before jittering
* `grid.cellsY`: `number` - number of cells in column
* `grid.cellsX`: `number` - number of cells in row
* `grid.points`: `number[][]` - coordinates `[x, y]` based on jittered square grid. Numbers rounded to 2 decimals
* `grid.boundary`: `number[][]` - off-canvas points coordinates used to cut the diagram approximately by canvas edges. Integers
* `grid.cells`: `{}` - cells data object, including voronoi data:
* * `grid.cells.i`: `number[]` - cell indexes `Uint16Array` or `Uint32Array` (depending on cells number)
* * `grid.cells.c`: `number[][]` - indexes of cells adjacent to each cell (neighboring cells)
* * `grid.cells.v`: `number[][]` - indexes of vertices of each cell
* * `grid.cells.b`: `number[]` - indicates if cell borders map edge, 1 if `true`, 0 if `false`. Integers, not Boolean
* `grid.vertices`: `{}` - vertices data object, contains only voronoi data:
* * `grid.vertices.p`: `number[][]` - vertices coordinates `[x, y]`, integers
* * `grid.vertices.c`: `number[][]` - indexes of cells adjacent to each vertex, each vertex has 3 adjacent cells
* * `grid.vertices.v`: `number[][]` - indexes of vertices adjacent to each vertex. Most vertices have 3 neighboring vertices, bordering vertices has only 2, while the third is still added to the data as `-1`
### Pack object
* `pack.cells`: `{}` - cells data object, including voronoi data:
* * `pack.cells.i`: `number[]` - cell indexes `Uint16Array` or `Uint32Array` (depending on cells number)
* * `pack.cells.p`: `number[][]` - cells coordinates `[x, y]` after repacking. Numbers rounded to 2 decimals
* * `pack.cells.c`: `number[][]` - indexes of cells adjacent to each cell (neighboring cells)
* * `pack.cells.v`: `number[][]` - indexes of vertices of each cell
* * `pack.cells.b`: `number[]` - indicator whether the cell borders the map edge, 1 if `true`, 0 if `false`. Integers, not Boolean
* * `pack.cells.g`: `number[]` - indexes of a source cell in `grid`. `Uint16Array` or `Uint32Array`. The only way to find correct `grid` cell parent for `pack` cells
* `pack.vertices`: `{}` - vertices data object, contains only voronoi data:
* * `pack.vertices.p`: `number[][]` - vertices coordinates `[x, y]`, integers
* * `pack.vertices.c`: `number[][]` - indexes of cells adjacent to each vertex, each vertex has 3 adjacent cells
* * `pack.vertices.v`: `number[][]` - indexes of vertices adjacent to each vertex. Most vertices have 3 neighboring vertices, bordering vertices has only 2, while the third is still added to the data as `-1`
## Features data
Features represent separate locked areas like islands, lakes and oceans.
### Grid object
* `grid.features`: `object[]` - array containing objects for all enclosed entities of original graph: islands, lakes and oceans. Feature object structure:
* * `i`: `number` - feature id starting from `1`
* * `land`: `boolean` - `true` if feature is land (height >= `20`)
* * `border`: `boolean` - `true` if feature touches map border (used to separate lakes from oceans)
* * `type`: `string` - feature type, can be `ocean`, `island` or `lake
### Pack object
* `pack.features`: `object[]` - array containing objects for all enclosed entities of repacked graph: islands, lakes and oceans. Note: element 0 has no data. Stored in .map file. Feature object structure:
* * `i`: `number` - feature id starting from `1`
* * `land`: `boolean` - `true` if feature is land (height >= `20`)
* * `border`: `boolean` - `true` if feature touches map border (used to separate lakes from oceans)
* * `type`: `string` - feature type, can be `ocean`, `island` or `lake`
* * `group`: `string`: feature subtype, depends on type. Subtype for ocean is `ocean`; for land it is `continent`, `island`, `isle` or `lake_island`; for lake it is `freshwater`, `salt`, `dry`, `sinkhole` or `lava`
* * `cells`: `number` - number of cells in feature
* * `firstCell`: `number` - index of the first (top left) cell in feature
* * `vertices`: `number[]` - indexes of vertices around the feature (perimetric vertices)
** `name`: `string` - name, available for `lake` type only
## Specific cells data
World data is mainly stored in typed arrays within `cells` object in both `grid` and `pack`.
### Grid object
* `grid.cells.h`: `number[]` - cells elevation in `[0, 100]` range, where `20` is the minimal land elevation. `Uint8Array`
* `grid.cells.f`: `number[]` - indexes of feature. `Uint16Array` or `Uint32Array` (depending on cells number)
* `grid.cells.t`: `number[]` - [distance field](https://prideout.net/blog/distance_fields/) from water level. `1, 2, ...` - land cells, `-1, -2, ...` - water cells, `0` - unmarked cell. `Uint8Array`
* `grid.cells.temp`: `number[]` - cells temperature in Celsius. `Uint8Array`
* `grid.cells.prec`: `number[]` - cells precipitation in unspecified scale. `Uint8Array`
### Pack object
* `pack.cells.h`: `number[]` - cells elevation in `[0, 100]` range, where `20` is the minimal land elevation. `Uint8Array`
* `pack.cells.f`: `number[]` - indexes of feature. `Uint16Array` or `Uint32Array` (depending on cells number)
* `pack.cells.t`: `number[]` - distance field. `1, 2, ...` - land cells, `-1, -2, ...` - water cells, `0` - unmarked cell. `Uint8Array`
* `pack.cells.s`: `number[]` - cells score. Scoring is used to define best cells to place a burg. `Uint16Array`
* `pack.cells.biome`: `number[]` - cells biome index. `Uint8Array`
* `pack.cells.burg`: `number[]` - cells burg index. `Uint16Array`
* `pack.cells.culture`: `number[]` - cells culture index. `Uint16Array`
* `pack.cells.state`: `number[]` - cells state index. `Uint16Array`
* `pack.cells.province`: `number[]` - cells province index. `Uint16Array`
* `pack.cells.religion`: `number[]` - cells religion index. `Uint16Array`
* `pack.cells.area`: `number[]` - cells area in pixels. `Uint16Array`
* `pack.cells.pop`: `number[]` - cells population in population points (1 point = 1000 people by default). `Float32Array`, not rounded to not lose population of high population rate
* `pack.cells.r`: `number[]` - cells river index. `Uint16Array`
* `pack.cells.fl`: `number[]` - cells flux amount. Defines how much water flow through the cell. Use to get rivers data and score cells. `Uint16Array`
* `pack.cells.conf`: `number[]` - cells flux amount in confluences. Confluences are cells where rivers meet each other. `Uint16Array`
* `pack.cells.harbor`: `number[]` - cells harbor score. Shows how many water cells are adjacent to the cell. Used for scoring. `Uint8Array`
* `pack.cells.haven`: `number[]` - cells haven cells index. Each coastal cell has haven cells defined for correct routes building. `Uint16Array` or `Uint32Array` (depending on cells number)
* `pack.cells.routes`: `object` - cells connections via routes. E.g. `pack.cells.routes[8] = {9: 306, 10: 306}` shows that cell `8` has two route connections - with cell `9` via route `306` and with cell `10` by route `306`
* `pack.cells.q`: `object` - quadtree used for fast closest cell detection
# Secondary data
Secondary data available as a part of the `pack` object.
## Cultures
Cultures (races, language zones) data is stored as an array of objects with strict element order. Element 0 is reserved by the _wildlands_ culture. If culture is removed, the element is not getting removed, but instead a `removed` attribute is added. Object structure:
* `i`: `number` - culture id, always equal to the array index
* `base`: `number` - _nameBase_ id, name base is used for names generation
* `name`: `string` - culture name
* `origins`: `number[]` - ids of origin cultures. Used to render cultures tree to show cultures evolution. The first array member is main link, other - supporting out-of-tree links
* `shield`: `string` - shield type. Used for emblems rendering
* `center`: `number` - cell id of culture center (initial cell)
* `code`: `string` - culture name abbreviation. Used to render cultures tree
* `color`: `string` - culture color in hex (e.g. `#45ff12`) or link to hatching pattern (e.g. `url(#hatch7)`)
* `expansionism`: `number` - culture growth multiplier. Used mainly during cultures generation to spread cultures not uniformly
* `type`: `string` - culture type, see [culture types](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Culture-types)
* `area`: `number` - culture area in pixels
* `cells`: `number` - number of cells assigned to culture
* `rural`: `number` - rural (non-burg) population of cells assigned to culture. In population points
* `urban`: `number` - urban (burg) population of cells assigned to culture. In population points
* `lock`: `boolean` - `true` if culture is locked (not affected by regeneration)
* `removed`: `boolean` - `true` if culture is removed
## Burgs
Burgs (settlements) data is stored as an array of objects with strict element order. Element 0 is an empty object. If burg is removed, the element is not getting removed, but instead a `removed` attribute is added. Object structure:
* `i`: `number` - burg id, always equal to the array index
* `name`: `string` - burg name
* `cell`: `number` - burg cell id. One cell can have only one burg
* `x`: `number` - x axis coordinate, rounded to two decimals
* `y`: `number` - y axis coordinate, rounded to two decimals
* `culture`: `number` - burg culture id
* `state`: `number` - burg state id
* `feature`: `number` - burg feature id (id of a landmass)
* `population`: `number` - burg population in population points
* `type`: `string` - burg type, see [culture types](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Culture_types)
* `coa`: `object` - emblem object, data model is the same as in [Armoria](https://github.com/Azgaar/Armoria) and covered in [API documentation](https://github.com/Azgaar/armoria-api#readme). The only additional fields are optional `size`: `number`, `x`: `number` and `y`: `number` that controls the emblem position on the map (if it's not default). If emblem is loaded by user, then the value is `{ custom: true }` and cannot be displayed in Armoria
* `MFCG`: `number` - burg seed in [Medieval Fantasy City Generator](https://watabou.github.io/city-generator) (MFCG). If not provided, seed is combined from map seed and burg id
* `link`: `string` - custom link to burg in MFCG. `MFCG` seed is not used if link is provided
* `capital`: `number` - `1` if burg is a capital, `0` if not (each state has only 1 capital)
* `port`: `number` - if burg is not a port, then `0`, otherwise feature id of the water body the burg stands on
* `citadel`: `number` - `1` if burg has a castle, `0` if not. Used for MFCG
* `plaza`: `number` - `1` if burg has a marketplace, `0` if not. Used for MFCG
* `shanty`: `number` - `1` if burg has a shanty town, `0` if not. Used for MFCG
* `temple`: `number` - `1` if burg has a temple, `0` if not. Used for MFCG
* `walls`: `number` - `1` if burg has walls, `0` if not. Used for MFCG
* `lock`: `boolean` - `true` if burg is locked (not affected by regeneration)
* `removed`: `boolean` - `true` if burg is removed
## States
States (countries) data is stored as an array of objects with strict element order. Element 0 is reserved for `neutrals`. If state is removed, the element is not getting removed, but instead a `removed` attribute is added. Object structure:
* `i`: `number` - state id, always equal to the array index
* `name`: `string` - short (proper) form of the state name
* `form`: `string` - state form type. Available types are `Monarchy`, `Republic`, `Theocracy`, `Union`, and `Anarchy`
* `formName`: `string` - string form name, used to get state `fullName`
* `fullName`: `string` - full state name. Combination of the proper name and state `formName`
* `color`: `string` - state color in hex (e.g. `#45ff12`) or link to hatching pattern (e.g. `url(#hatch7)`)
* `center`: `number` - cell id of state center (initial cell)
* `pole`: `number[]` - state pole of inaccessibility (visual center) coordinates, see [the concept description](https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc?gi=6bd4fcb9ecc1)
* `culture`: `number` - state culture id (equals to initial cell culture)
* `type`: `string` - state type, see [culture types](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Culture types)
* `expansionism`: `number` - state growth multiplier. Used mainly during state generation to spread states not uniformly
* `area`: `number` - state area in pixels
* `burgs`: `number` - number of burgs within the state
* `cells`: `number` - number of cells within the state
* `rural`: `number` - rural (non-burg) population of state cells. In population points
* `urban`: `number` - urban (burg) population of state cells. In population points
* `neighbors`: `number[]` - ids of neighboring (bordering by land) states
* `provinces`: `number[]` - ids of state provinces
* `diplomacy`: `string[]` - diplomatic relations status for all states. 'x' for self and neutrals. Element 0 (neutrals) `diplomacy` is used differently and contains wars story as `string[][]`
* `campaigns`: `object[]` - wars the state participated in. The was is defined as `start`: `number` (year), `end`: `number` (year), `name`: `string`
* `alert`: `number` - state war alert, see [military forces page](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Military-Forces)
* `military`: `Regiment[]` - list of state regiments, see [military forces page](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Military-Forces)
* `coa`: `object` - emblem object, data model is the same as in [Armoria](https://github.com/Azgaar/Armoria) and covered in [API documentation](https://github.com/Azgaar/armoria-api#readme). The only additional fields are optional `size`: `number`, `x`: `number` and `y`: `number` that controls the emblem position on the map (if it's not default). If emblem is loaded by user, then the value is `{ custom: true }` and cannot be displayed in Armoria
* `lock`: `boolean` - `true` if state is locked (not affected by regeneration)
* `removed`: `boolean` - `true` if state is removed
### Regiment
* `i`: `number` - regiment id, equals to the array index of regiment in the `state[x].military` array. Not unique, as unique string `regimentStateId-regimentId` is used
* `x`: `number` - regiment x coordinate
* `y`: `number` - regiment y coordinate
* `bx`: `number` - regiment base x coordinate
* `by`: `number` - regiment base y coordinate
* `angle`: `number` - regiment rotation angle degree
* `icon`: `number` - Unicode character to serve as an icon
* `cell`: `number` - original regiment cell id
* `state`: `number` - regiment state id
* `name`: `string` - regiment name
* `n`: `number` - `1` if regiment is a separate unit (like naval units), `0` is not
* `u`: `Record<unitName, number>` - regiment content object
## Provinces
Provinces data is stored as an array of objects with strict element order. Element 0 is not used. If religion is removed, the element is not getting removed, but instead a `removed` attribute is added. Object structure:
* `i`: `number` - province id, always equal to the array index
* `name`: `string` - short (proper) form of the province name
* `formName`: `string` - string form name, used to get province `fullName`
* `fullName`: `string` - full state name. Combination of the proper name and province `formName`
* `color`: `string` - province color in hex (e.g. `#45ff12`) or link to hatching pattern (e.g. `url(#hatch7)`)
* `center`: `number` - cell id of province center (initial cell)
* `pole`: `number[]` - province pole of inaccessibility (visual center) coordinates, see [the concept description](https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc?gi=6bd4fcb9ecc1)
* `area`: `number` - province area in pixels
* `burg`: `number` - id of province capital burg if any
* `burgs`: `number[]` - id of burgs within the province. Optional (added when Province editor is opened)
* `cells`: `number` - number of cells within the province
* `rural`: `number` - rural (non-burg) population of province cells. In population points
* `urban`: `number` - urban (burg) population of state province. In population points
* `coa`: `object` - emblem object, data model is the same as in [Armoria](https://github.com/Azgaar/Armoria) and covered in [API documentation](https://github.com/Azgaar/armoria-api#readme). The only additional fields are optional `size`: `number`, `x`: `number` and `y`: `number` that controls the emblem position on the map (if it's not default). If emblem is loaded by user, then the value is `{ custom: true }` and cannot be displayed in Armoria
* `lock`: `boolean` - `true` if province is locked (not affected by regeneration)
* `removed`: `boolean` - `true` if province is removed
## Religions
Religions data is stored as an array of objects with strict element order. Element 0 is reserved for "No religion". If province is removed, the element is not getting removed, but instead a `removed` attribute is added. Object structure:
* `i`: `number` - religion id, always equal to the array index
* `name`: `string` - religion name
* `type`: `string` - religion type. Available types are `Folk`, `Organized`, `Heresy` and `Cult`
* `form`: `string` - religion form
* `deity`: `string` - religion supreme deity if any
* `color`: `string` - religion color in hex (e.g. `#45ff12`) or link to hatching pattern (e.g. `url(#hatch7)`)
* `code`: `string` - religion name abbreviation. Used to render religions tree
* `origins`: `number[]` - ids of ancestor religions. `[0]` if religion doesn't have an ancestor. Used to render religions tree. The first array member is main link, other - supporting out-of-tree links
* `center`: `number` - cell id of religion center (initial cell)
* `culture`: `number` - religion original culture
* `expansionism`: `number` - religion growth multiplier. Used during religion generation to define competitive size
* `expansion`: `string` - religion expansion type. Can be `culture` so that religion grow only within its culture or `global`
* `area`: `number` - religion area in pixels
* `cells`: `number` - number of cells within the religion
* `rural`: `number` - rural (non-burg) population of religion cells. In population points
* `urban`: `number` - urban (burg) population of state religion. In population points
* `lock`: `boolean` - `true` if religion is locked (not affected by regeneration)
* `removed`: `boolean` - `true` if religion is removed
## Rivers
Rivers data is stored as an unordered array of objects (so element id is _not_ the array index). Object structure:
* `i`: `number` - river id
* `name`: `string` - river name
* `type`: `string` - river type, used to get river full name only
* `source`: `number` - id of cell at river source
* `mouth`: `number` - id of cell at river mouth
* `parent`: `number` - parent river id. If river doesn't have a parent, the value is self id or `0`
* `basin`: `number` - river basin id. Basin id is a river system main stem id. If river doesn't have a parent, the value is self id
* `cells`: `number[]` - if of river points cells. Cells may not be unique. Cell value `-1` means the river flows off-canvas
* `points`: `number[][]` - river points coordinates. Auto-generated rivers don't have points stored and rely on `cells` for rendering
* `discharge`: `number` - river flux in m3/s
* `length`: `number` - river length in km
* `width`: `number` - river mouth width in km
* `sourceWidth`: `number` - additional width added to river source on rendering. Used to make lake outlets start with some width depending on flux. Can be also used to manually create channels
## Markers
Markers data is stored as an unordered array of objects (so element id is _not_ the array index). Object structure:
* `i`: `number` - marker id. `'marker' + i` is used as svg element id and marker reference in `notes` object
* `icon`: `number` - Unicode character (usually an [emoji](https://emojipedia.org/)) to serve as an icon
* `x`: `number` - marker x coordinate
* `y`: `number` - marker y coordinate
* `cell`: `number` - cell id, used to prevent multiple markers generation in the same cell
* `type`: `string` - marker type. If set, style changes will be applied to all markers of the same type. Optional
* `size`: `number` - marker size in pixels. Optional, default value is `30` (30px)
* `fill`: `string` - marker pin fill color. Optional, default is `#fff` (white)
* `stroke`: `string` - marker pin stroke color. Optional, default is `#000` (black)
* `pin`: `string`: pin element type. Optional, default is `bubble`. Pin is not rendered if value is set to `no`
* `pinned`: `boolean`: if any marker is pinned, then only markers with `pinned = true` will be rendered. Optional
* `dx`: `number` - icon x shift percent. Optional, default is `50` (50%, center)
* `dy`: `number` - icon y shift percent. Optional, default s `50` (50%, center)
* `px`: `number` - icon font-size in pixels. Optional, default is `12` (12px)
* `lock`: `boolean` - `true` if marker is locked (not affected by regeneration). Optional
## Routes
Routes data is stored as an unordered array of objects (so element id is _not_ the array index). Object structure:
* `i`: `number` - route id. Please note the element with id `0` is a fully valid route, not a placeholder
* `points`: `number[]` - array of control points in format `[x, y, cellId]`
* `feature`: `number` - feature id of the route. Auto-generated routes cannot be place on multiple features
* `group`: `string` - route group. Default groups are: 'roads', 'trails', 'searoutes'
* `length`: `number` - route length in km. Optional
* `name`: `string` - route name. Optional
* `lock`: `boolean` - `true` if route is locked (not affected by regeneration). Optional
## Zones
Zones data is stored as an array of objects with `i` not necessary equal to the element index, but order of element defines the rendering order and is important. Object structure:
* `i`: `number` - zone id. Please note the element with id `0` is a fully valid zone, not a placeholder
* `name`: `string` - zone description
* `type`: `string` - zone type
* `color`: `string` - link to hatching pattern (e.g. `url(#hatch7)`) or color in hex (e.g. `#45ff12`)
* `cells`: `number[]` - array of zone cells
* `lock`: `boolean` - `true` if zone is locked (not affected by regeneration). Optional
* `hidden`: `boolean` - `true` if zone is hidden (not displayed). Optional
# Secondary global data
Secondary data exposed to global space.
## Biomes
Biomes data object is globally available as `biomesData`. It stores a few arrays, making it different from other data. Object structure:
* `i`: `number[]` - biome id
* `name`: `string[]` - biome names
* `color`: `string[]` - biome colors in hex (e.g. `#45ff12`) or link to hatching pattern (e.g. `url(#hatch7)`)
* `biomesMartix`: `number[][]` - 2d matrix used to define cell biome by temperature and moisture. Columns contain temperature data going from > `19` °C to < `-4` °C. Rows contain data for 5 moisture bands from the drier to the wettest one. Each row is a `Uint8Array`
* `cost`: `number[]` - biome movement cost, must be `0` or positive. Extensively used during cultures, states and religions growth phase. `0` means spread to this biome costs nothing. Max value is not defined, but `5000` is the actual max used by default
* `habitability`: `number[]` - biome habitability, must be `0` or positive. `0` means the biome is uninhabitable, max value is not defined, but `100` is the actual max used by default
* `icons`: `string[][]` - non-weighed array of icons for each biome. Used for _relief icons_ rendering. Not-weighed means that random icons from array is selected, so the same icons can be mentioned multiple times
* `iconsDensity`: `number[]` - defines how packed icons can be for the biome. An integer from `0` to `150`
## Notes
Notes (legends) data is stored in unordered array of objects: `notes`. Object structure is as simple as:
* `i`: `string` - note id
* `name`: `string` - note name, visible in Legend box
* `legend`: `string` - note text in html
## Name bases
Name generator consumes training sets of real-world town names (with the exception of fantasy name bases) stored in `nameBases` array, that is available globally. Each array element represent a separate base. Base structure is:
* `i`: `number` - base id, always equal to the array index
* `name`: `string` - names base proper name
* `b`: `string` - long string containing comma-separated list of names
* `min`: `number` - recommended minimal length of generated names. Generator will adding new syllables until min length is reached
* `max`: `number` - recommended maximal length of generated names. If max length is reached, generator will stop adding new syllables
* `d`: `string` - letters that are allowed to be duplicated in generated names
* `m`: `number` - if multi-word name is generated, how many of this cases should be transformed into a single word. `0` means multi-word names are not allowed, `1` - all generated multi-word names will stay as they are
## Property Availability Timeline
Properties are added to grid/pack progressively during map generation. Understanding when each property becomes available is crucial for module dependencies and avoiding undefined references.
### Grid Properties (coarse mesh ~10K cells)
- `cells.h` - Available after: heightmap module
- `cells.f` - Available after: features markupGrid module
- `cells.t` - Available after: features markupGrid module
- `cells.temp` - Available after: geography temperature calculation
- `cells.prec` - Available after: geography precipitation generation
### Pack Properties (refined mesh)
- `cells.h` - Available after: pack generation (reGraph utility)
- `cells.f` - Available after: features markupPack module
- `cells.t` - Available after: features markupPack module
- `cells.fl` - Available after: rivers module
- `cells.r` - Available after: rivers module
- `cells.biome` - Available after: biomes module
- `cells.s` - Available after: cell ranking utility
- `cells.pop` - Available after: cell ranking utility
- `cells.culture` - Available after: cultures module
- `cells.burg` - Available after: burgs-and-states module
- `cells.state` - Available after: burgs-and-states module
- `cells.religion` - Available after: religions module
- `cells.province` - Available after: provinces module
### Module Execution Flow
```mermaid
flowchart TD
A[Generate Grid] --> B[Heightmap Module]
B --> |adds cells.h| C[Features markupGrid]
C --> |adds cells.f, cells.t| D[Geography Temperature]
D --> |adds cells.temp| E[Geography Precipitation]
E --> |adds cells.prec| F[Pack Generation - reGraph]
F --> |refines cells.h| G[Features markupPack]
G --> |adds pack cells.f, cells.t| H[Rivers Module]
H --> |adds cells.fl, cells.r| I[Biomes Module]
I --> |adds cells.biome| J[Cell Ranking]
J --> |adds cells.s, cells.pop| K[Cultures Module]
K --> |adds cells.culture, pack.cultures| L[Cultures Expand]
L --> M[BurgsAndStates Module]
M --> |adds cells.burg, cells.state, pack.burgs, pack.states| N[Routes Module]
N --> O[Religions Module]
O --> |adds cells.religion, pack.religions| P[State Forms]
P --> Q[Provinces Module]
Q --> |adds cells.province, pack.provinces| R[Burg Features]
R --> S[Rivers Specify]
S --> T[Features Specify]
T --> U[Military Module]
U --> V[Markers Module]
V --> W[Zones Module]
W --> X[Complete Map Data]
```

View file

@ -0,0 +1,226 @@
# Heightmap Generator Assessment
## Executive Summary
The heightmap generator module has undergone significant refactoring during the port from the browser-based version to the headless engine. While the core logic remains intact, several critical deviations have been introduced that impact functionality, architecture consistency, and maintainability. Most critically, the naming convention has become inconsistent and the state management pattern has been fundamentally altered.
## Key Deviations Identified
### 1. **Critical Naming Convention Violations**
**Original Pattern (../modules/heightmap-generator.js):**
- **Input parameter:** `graph` (lines 9, 24, 40, 64, 70) - represents the incoming data structure
- **Internal variable:** `grid` (line 4, 14, 21) - closure variable storing the graph after `setGraph(graph)` call
- **Usage pattern:** Functions receive `graph`, call `setGraph(graph)` to store as `grid`, then use `grid` throughout
**Current Broken Pattern (src/engine/modules/heightmap-generator.js):**
- **Inconsistent parameter naming:** Mix of `grid` and `graph` parameters
- **Line 178:** `addPit(heights, graph, ...)` - should be `grid` like other functions
- **Line 254, 255:** `findGridCell(startX, startY, graph)` - uses undefined `graph` variable
- **Line 347:** `findGridCell(startX, startY, graph)` - uses undefined `graph` variable
- **Line 444, 445:** `findGridCell(startX, startY, graph)` - uses undefined `graph` variable
**CRITICAL BUG:** These functions will fail at runtime because `graph` is undefined in their scope.
### 2. **setGraph/setGrid State Management Deviation**
**Original setGraph Pattern:**
```javascript
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; // Store graph as grid for internal use
};
```
**Current setGrid Pattern:**
```javascript
function setGrid(grid, utils) {
const { createTypedArray } = utils;
const { cellsDesired, cells, points } = grid;
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 }; // Returns computed values instead of storing state
}
```
**Critical Differences:**
1. **State Storage:** Original stores state in closure, current returns computed values
2. **Naming:** Original uses `graph` parameter name, current uses `grid`
3. **Function Name:** `setGraph` vs `setGrid` - breaks the original naming logic
4. **Return Pattern:** Original modifies closure state, current returns data for functional approach
### 3. **Architectural Pattern Shift Analysis**
**Original Closure-Based State Management:**
- State variables (`grid`, `heights`, `blobPower`, `linePower`) live in module closure
- `setGraph(graph)` initializes state once per generation cycle
- Helper functions access closure state directly (no parameters needed)
- `clearData()` cleans up state after generation
**Current Pure Functional Approach:**
- No persistent state - everything passed as parameters
- Each function receives `(heights, grid, blobPower, config, utils, ...args)`
- `setGrid(grid, utils)` computes values and returns them (no state storage)
- Each helper function creates new arrays and returns modified results
**Impact Analysis:**
- **Positive:** True functional purity enables better testing and no side effects
- **Negative:** Massive parameter bloat (8+ parameters per function vs 0 in original)
- **Performance:** Multiple array allocations vs single state initialization
### 4. **Parameter Propagation Problems**
**Missing Parameters:**
- Line 90-92: `modify()` function call missing `power` parameter that's used in implementation
- Line 92: `modify(heights, a3, +a2, 1, utils)` - missing `power` but function expects it
**Wrong Parameter Order:**
- Functions expect `(heights, grid, ...)` but some calls pass different structures
- Type mismatches between expected `grid` object and passed `graph` references
### 5. **Return Value Handling Issues**
**Critical Deviation:**
- Original functions modified global `heights` array in place
- Current functions create new `Uint8Array(heights)` copies but don't always maintain referential consistency
- This could lead to performance issues and memory overhead
### 6. **Utility Dependencies**
**Incomplete Migration:**
- Line 51: `fromPrecreated` function is completely stubbed out
- Missing critical browser-to-headless migration for image processing
- DOM dependencies (`document.createElement`, `canvas`, `Image`) not replaced
## Specific Runtime Failures
### Bug 1: Undefined Variable References (CRITICAL)
```javascript
// Line 178 - Function parameter name
export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
// Line 199 - Internal usage tries to access 'grid' (UNDEFINED)
start = findGridCell(x, y, grid); // ReferenceError: grid is not defined
```
### Bug 2: Parameter/Variable Mismatch Pattern
**Broken Functions:**
- `addPit` (line 178): parameter `graph`, usage `grid` (line 199, 209)
- `addRange` (line 221): parameter `grid`, but calls `findGridCell(x, y, graph)` (lines 254-255)
- `addTrough` (line 320): parameter `grid`, but calls `findGridCell(x, y, graph)` (lines 347, 359)
- `addStrait` (line 425): parameter `grid`, but calls `findGridCell(x, y, graph)` (lines 444-445)
### Bug 3: Missing Parameter in Function Calls
```javascript
// Line 90 - Call site
if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
// Line 490 - Function signature expects 6 parameters, gets 5
export function modify(heights, range, add, mult, power, utils) {
// ^^^^^ undefined
```
### Bug 4: Inconsistent Array Handling
```javascript
// Every helper function does:
heights = new Uint8Array(heights); // Unnecessary copying if already Uint8Array
// Original pattern: direct mutation of closure variable
```
## Performance Impact Assessment
1. **Memory Overhead:** Each helper function creates new Uint8Array copies
2. **Parameter Bloat:** Functions now take 6-8 parameters instead of accessing closure variables
3. **Reduced Efficiency:** Multiple array allocations per generation step
## Recommendations
### Critical Fixes (Must Fix Immediately)
#### 1. **Restore Original Naming Convention**
**All functions must use the original pattern:**
- **Parameter name:** `graph` (not `grid`)
- **Internal usage:** `grid` (converted from `graph` parameter)
- **Function name:** `setGraph` (not `setGrid`)
```javascript
// CORRECT pattern matching original:
export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
const grid = graph; // Convert parameter to internal variable name
// ... use grid throughout function body
}
```
#### 2. **Fix Parameter/Variable Mismatches**
**Every function with graph/grid issues:**
- Line 178: `addPit` - change parameter from `graph` to `grid` OR add `const grid = graph;`
- Lines 254-255, 347, 359, 444-445: Change `graph` to `grid` in `findGridCell` calls
- Line 92: Add missing `power` parameter to `modify()` call
#### 3. **Standardize Function Signatures**
**All helper functions should follow this pattern:**
```javascript
export function addHill(heights, graph, blobPower, config, utils, ...specificArgs) {
const grid = graph; // Mirror original internal conversion
// ... implementation using grid
}
```
### Architecture Decision Points
#### Option A: Pure Functional (Current Broken Approach)
**Pros:** No side effects, better testability
**Cons:** 8+ parameters per function, performance overhead, complexity
**Fix Required:** Complete parameter standardization
#### Option B: Hybrid Closure Pattern (Recommended)
**Restore original naming but keep functional returns:**
```javascript
function setGraph(graph, utils) { // Restore original name
const grid = graph; // Original internal conversion
const { cellsDesired, cells, points } = grid;
// ... compute values
return { heights, blobPower, linePower, grid }; // Include grid in return
}
```
#### Option C: Context Object Pattern
**Bundle related parameters:**
```javascript
export function addHill(context, count, height, rangeX, rangeY) {
const { heights, graph, blobPower, config, utils } = context;
const grid = graph; // Maintain original pattern
// ... implementation
}
```
## Conclusion
The heightmap generator refactoring represents an **incomplete and broken migration** from the original closure-based pattern. While the functional approach has merit, the implementation violates the original naming convention and introduces multiple runtime failures. The core issue is that the refactoring was performed without understanding the original `graph``grid` naming logic.
**Root Cause:** The original code used `graph` as the input parameter name and `grid` as the internal variable name after calling `setGraph(graph)`. The current version inconsistently mixes these names, creating undefined variable references.
**Severity:** HIGH - Multiple functions will fail at runtime due to undefined variable access.
## Priority Actions (In Order)
### Immediate (Blocking)
1. **Fix undefined variable references** - All `findGridCell(x, y, graph)` calls where `graph` is undefined
2. **Standardize parameter names** - Either all `graph` or all `grid`, but consistently applied
3. **Restore setGraph naming** - Change `setGrid` back to `setGraph` to match original pattern
4. **Fix missing parameters** - Add `power` parameter to `modify()` function calls
### Short Term
1. **Choose architectural pattern** - Pure functional vs hybrid vs context object
2. **Optimize array handling** - Eliminate unnecessary Uint8Array copying
3. **Complete parameter standardization** - Ensure all functions follow chosen pattern
### Long Term
1. **Complete fromPrecreated migration** - Implement headless image processing
2. **Performance benchmarking** - Compare against original implementation
3. **Add comprehensive testing** - Prevent regression of these naming issues
**Recommendation:** Restore the original `graph` parameter → `grid` internal variable pattern throughout the entire module to maintain consistency with the original design intent.

View file

@ -0,0 +1,460 @@
# Exhaustive Step-by-Step Trace of Fantasy Map Generator Execution Flow
## Starting Point: "Generate Map" Button Click
**File**: `/home/user/Fantasy-Map-Generator/procedural/index.html`
- **Line 262**: `<button id="newMapButton" class="primary">🗺️ Generate Map</button>`
- **Line 263**: `<button id="generateButton" class="primary">Generate (Alt)</button>`
## Phase 1: Event Handler Registration and Initialization
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/main.js`
### Step 1: DOM Content Loaded
- **Line 210**: `window.addEventListener('DOMContentLoaded', () => { ... })`
- **Data Created**: DOM content loaded event listener
- **Function Called**: Anonymous function for initialization
### Step 2: Button Event Handler Registration
- **Lines 222-225**: Button event handler registration
```javascript
const generateBtn = byId("newMapButton") || byId("generateButton");
if (generateBtn) {
generateBtn.addEventListener("click", handleGenerateClick);
}
```
- **Data Created**: Event listener for click event
- **Function Called**: `handleGenerateClick` when clicked
## Phase 2: Configuration Building and Validation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/main.js`
### Step 3: Generate Click Handler Starts
- **Line 32**: `async function handleGenerateClick()` starts execution
- **Function Called**: `handleGenerateClick()`
### Step 4: Build Configuration from UI
- **Line 36**: `const config = buildConfigFromUI();`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/config-builder.js`
- **Function Called**: `buildConfigFromUI()` (Line 8)
- **Data Created**: Complete configuration object with sections:
### Step 5: Configuration Object Structure Created
- **Lines 9-31** in config-builder.js: Configuration object structure created
```javascript
const config = {
seed: getSeed(), // Line 61: Gets seed from UI or generates new one
graph: buildGraphConfig(), // Line 67: { width, height, cellsDesired }
map: buildMapConfig(), // Line 79: { coordinatesSize, latitude }
heightmap: buildHeightmapConfig(), // Line 86: { templateId }
temperature: buildTemperatureConfig(), // Line 93: { heightExponent, temperatureScale, temperatureBase }
precipitation: buildPrecipitationConfig(), // Line 101: { winds, moisture }
features: {},
biomes: {},
lakes: buildLakesConfig(), // Line 110: { lakeElevationLimit, heightExponent }
rivers: buildRiversConfig(), // Line 119: { resolveDepressionsSteps, cellsCount }
oceanLayers: buildOceanLayersConfig(), // Line 129: { outline }
cultures: buildCulturesConfig(), // Line 137: { culturesInput, culturesSet, emblemShape, etc. }
burgs: buildBurgsConfig(), // Line 162: { statesNumber, manorsInput, growthRate, etc. }
religions: buildReligionsConfig(), // Line 178: { religionsNumber, growthRate }
provinces: buildProvincesConfig(), // Line 185: { provincesRatio }
military: buildMilitaryConfig(), // Line 192: { year, eraShort, era }
markers: buildMarkersConfig(), // Line 196: { culturesSet }
zones: buildZonesConfig(), // Line 202: { globalModifier }
debug: buildDebugConfig() // Line 208: { TIME, WARN, INFO, ERROR }
};
```
### Step 6: Configuration Validation
- **Line 39** in main.js: `const { fixed, originalValidation, fixedValidation, wasFixed } = validateAndFix(config);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/config-validator.js`
- **Function Called**: `validateAndFix(config)`
- **Data Created**: Validation results and fixed configuration
### Step 7: Validation Logging
- **Lines 42-60** in main.js: Validation logging and error handling
- **Objects Received**: `originalValidation`, `fixedValidation`, `wasFixed` boolean
- **Expected**: Validation objects with `errors`, `warnings`, `valid` properties
### Step 8: Save Configuration to LocalStorage
- **Line 64**: `localStorage.setItem('fmg-last-config', saveConfigToJSON(fixed));`
- **Function Called**: `saveConfigToJSON(fixed)` from config-builder.js
- **Data Created**: JSON string representation of configuration stored in localStorage
## Phase 3: Engine Generation Call
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/main.js`
### Step 9: Call Engine Generate Function
- **Line 70**: `const mapData = await generateMapEngine(fixed);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
- **Function Called**: `generate(config)` (imported as `generateMapEngine`, Line 33)
- **Data Passed**: Validated and fixed configuration object
## Phase 4: Engine Initialization
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 10: Start Performance Timer
- **Line 34**: `const timeStart = performance.now();`
- **Data Created**: Timestamp for performance measurement
### Step 11: Extract Debug Flags
- **Line 37**: `const { TIME, WARN, INFO } = config.debug;`
- **Data Extracted**: Debug flags from configuration
### Step 12: Seed Initialization
- **Line 40**: `const seed = config.seed || Utils.generateSeed();`
- **Function Called**: `Utils.generateSeed()` if no seed provided
- **Data Created**: Final seed value for generation
### Step 13: Initialize Seeded Random Number Generator
- **Line 41**: `Math.random = Utils.aleaPRNG(seed);`
- **Function Called**: `Utils.aleaPRNG(seed)` from `/home/user/Fantasy-Map-Generator/procedural/src/engine/utils/alea.js`
- **Data Modified**: Global Math.random function replaced with seeded PRNG
### Step 14: Console Group Start
- **Line 44**: `INFO && console.group("Generating Map with Seed: " + seed);`
- **Action**: Console group started if INFO debug flag is true
## Phase 5: Grid Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 15: Generate Initial Grid
- **Line 48**: `let grid = Graph.generateGrid(config.graph);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/utils/graph.js`
- **Function Called**: `generateGrid(config)` (Line 13)
- **Data Passed**: `config.graph` object containing `{ width, height, cellsDesired }`
- **Data Created**: Initial grid object
### Step 16: Grid Generation Process
- **Lines 15-17** in graph.js: Grid generation process
```javascript
const { spacing, cellsDesired, boundary, points, cellsX, cellsY } = placePoints(config);
const { cells, vertices } = calculateVoronoi(points, boundary);
return { spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices };
```
- **Functions Called**: `placePoints(config)` (Line 21), `calculateVoronoi(points, boundary)` (Line 34)
- **Data Created**: Voronoi diagram with cells and vertices
### Step 17: Point Placement
- **Lines 21-31** in graph.js: Point placement
- **Function Called**: `getBoundaryPoints()`, `getJitteredGrid()`
- **Data Created**: Array of points for Voronoi calculation
- **Objects Created**: `{ spacing, cellsDesired, boundary, points, cellsX, cellsY }`
### Step 18: Voronoi Calculation
- **Lines 34-50** in graph.js: Voronoi calculation
- **External Library**: Delaunator for Delaunay triangulation
- **Function Called**: `new Voronoi(delaunay, allPoints, points.length)`
- **Data Created**: `cells` and `vertices` objects with neighbor relationships
## Phase 6: Heightmap Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 19: Generate Heightmap
- **Line 51**: `grid.cells.h = await Heightmap.generate(grid, config, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/heightmap-generator.js`
- **Function Called**: `generate(graph, config, utils)` (Line 3)
- **Data Passed**: `grid` object, `config` object, `Utils` module
- **Data Created**: Height values array assigned to `grid.cells.h`
### Step 20: Heightmap Processing
- **Lines 3-18** in heightmap-generator.js: Heightmap generation
- **Data Extracted**: `templateId` from `config.heightmap`
- **Function Called**: `fromTemplate(graph, templateId, config, utils)` (Line 32)
- **Data Created**: Heights array for all grid cells
### Step 21: Template Processing
- **Lines 32-48** in heightmap-generator.js: Template processing
- **Data Accessed**: `heightmapTemplates[id].template` string
- **Function Called**: `setGraph(graph, utils)`, `addStep()` for each template step
- **Data Created**: Final heights array with template-based terrain
## Phase 7: Features Markup
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 22: Markup Grid Features
- **Line 52**: `grid = Features.markupGrid(grid, config, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/features.js`
- **Function Called**: `markupGrid(grid, config, utils)` (Line 28)
- **Data Passed**: Grid with heights, config, utils
- **Data Modified**: Grid object enhanced with feature information
### Step 23: Grid Markup Process
- **Lines 28-50** in features.js: Grid markup process
- **Data Created**: `distanceField` (Int8Array), `featureIds` (Uint16Array), `features` array
- **Algorithm**: Flood-fill to identify connected land/water regions
- **Data Added to Grid**: Distance fields and feature classifications
## Phase 8: Geography and Climate
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 24: Define Map Size
- **Line 55**: `const { mapCoordinates } = Geography.defineMapSize(grid, config, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/utils/geography.js`
- **Function Called**: `defineMapSize(grid, config, Utils)`
- **Data Created**: `mapCoordinates` object with geographic bounds
### Step 25: Add Lakes in Deep Depressions
- **Line 56**: `grid = Geography.addLakesInDeepDepressions(grid, config.lakes, Utils);`
- **Function Called**: `addLakesInDeepDepressions(grid, config.lakes, Utils)`
- **Data Modified**: Grid enhanced with lake information
### Step 26: Open Near-Sea Lakes
- **Line 57**: `grid = Geography.openNearSeaLakes(grid, config.lakes, Utils);`
- **Function Called**: `openNearSeaLakes(grid, config.lakes, Utils)`
- **Data Modified**: Lake connectivity to ocean processed
### Step 27: Calculate Temperatures
- **Line 60**: `const { temp } = Geography.calculateTemperatures(grid, mapCoordinates, config.temperature, Utils);`
- **Function Called**: `calculateTemperatures()`
- **Data Created**: Temperature array for all cells
- **Line 61**: `grid.cells.temp = temp;` - Temperature data assigned to grid
### Step 28: Generate Precipitation
- **Line 62**: `const { prec } = Geography.generatePrecipitation(grid, mapCoordinates, config.precipitation, Utils);`
- **Function Called**: `generatePrecipitation()`
- **Data Created**: Precipitation array for all cells
- **Line 63**: `grid.cells.prec = prec;` - Precipitation data assigned to grid
## Phase 9: Pack Generation (Refined Mesh)
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 29: Generate Refined Mesh (Pack)
- **Line 66**: `let pack = Graph.reGraph(grid, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/utils/graph.js`
- **Function Called**: `reGraph(grid, Utils)`
- **Data Created**: Refined mesh (`pack`) with higher resolution than grid
- **Purpose**: Creates detailed mesh for final map features
### Step 30: Markup Pack Features
- **Line 67**: `pack = Features.markupPack(pack, grid, config, Utils, { Lakes });`
- **Function Called**: `Features.markupPack()`
- **Data Passed**: Pack mesh, original grid, config, utils, Lakes module
- **Data Modified**: Pack enhanced with feature information
## Phase 10: River Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 31: Generate Rivers
- **Line 70**: `const riverResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names });`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/river-generator.js`
- **Function Called**: `generate(pack, grid, config.rivers, Utils, { Lakes, Names })`
- **Data Passed**: Pack mesh, grid, river config, utilities, Lakes and Names modules
- **Data Created**: River system data
### Step 32: Update Pack with Rivers
- **Line 71**: `pack = riverResult.pack;`
- **Data Modified**: Pack object updated with river information
## Phase 11: Biome Assignment
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 33: Define Biomes
- **Line 74**: `const { biome } = Biomes.define(pack, grid, config.biomes, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/biomes.js`
- **Function Called**: `define(pack, grid, config.biomes, Utils)`
- **Data Created**: Biome classifications for each cell
### Step 34: Assign Biomes to Pack
- **Line 75**: `pack.cells.biome = biome;`
- **Data Modified**: Biome data assigned to pack cells
## Phase 12: Cell Ranking and Population
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 35: Rank Cells
- **Line 78**: `const { s, pop } = Cell.rankCells(pack, Utils, { biomesData: Biomes.getDefault() });`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/utils/cell.js`
- **Function Called**: `rankCells(pack, Utils, { biomesData })`
- **Data Passed**: Pack, utilities, default biomes data
- **Data Created**: Cell suitability rankings (`s`) and population values (`pop`)
### Step 36: Assign Cell Rankings
- **Lines 79-80**: Cell data assignment
```javascript
pack.cells.s = s;
pack.cells.pop = pop;
```
## Phase 13: Culture Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 37: Generate Cultures
- **Line 83**: `const culturesResult = Cultures.generate(pack, grid, config.cultures, Utils, { Names });`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/cultures-generator.js`
- **Function Called**: `generate(pack, grid, config.cultures, Utils, { Names })`
- **Data Created**: Cultures data and culture assignments
### Step 38: Integrate Culture Data
- **Lines 84-85**: Culture data integration
```javascript
let packWithCultures = { ...pack, cultures: culturesResult.cultures };
packWithCultures.cells.culture = culturesResult.culture;
```
### Step 39: Expand Cultures
- **Line 87**: `const expandedCulturesData = Cultures.expand(packWithCultures, config.cultures, Utils, { biomesData: Biomes.getDefault() });`
- **Function Called**: `Cultures.expand()`
- **Data Created**: Expanded culture territories
### Step 40: Update Pack with Expanded Cultures
- **Line 88**: `pack = { ...packWithCultures, ...expandedCulturesData };`
- **Data Modified**: Pack updated with expanded culture data
## Phase 14: Burgs and States Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 41: Generate Burgs and States
- **Line 90**: `const burgsAndStatesResult = BurgsAndStates.generate(pack, grid, config.burgs, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/burgs-and-states.js`
- **Function Called**: `generate(pack, grid, config.burgs, Utils)`
- **Data Created**: Settlements (burgs) and political entities (states)
### Step 42: Integrate Burgs and States Data
- **Lines 91-97**: Burgs and states data integration
```javascript
pack = {
...pack,
burgs: burgsAndStatesResult.burgs,
states: burgsAndStatesResult.states
};
pack.cells.burg = burgsAndStatesResult.burg;
pack.cells.state = burgsAndStatesResult.state;
```
## Phase 15: Additional Features Generation
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 43: Generate Routes
- **Line 99**: `const routesResult = Routes.generate(pack, grid, Utils, []);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/routes-generator.js`
- **Function Called**: `generate(pack, grid, Utils, [])`
- **Data Created**: Trade and travel routes
### Step 44: Generate Religions
- **Line 102**: `const religionsResult = Religions.generate(pack, grid, config.religions, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/religions-generator.js`
- **Function Called**: `generate(pack, grid, config.religions, Utils)`
- **Data Created**: Religious systems and distributions
### Step 45: Define State Forms
- **Line 105**: `const stateFormsResult = BurgsAndStates.defineStateForms(undefined, pack, Utils);`
- **Function Called**: `BurgsAndStates.defineStateForms()`
- **Data Created**: Government forms for states
### Step 46: Generate Provinces
- **Line 108**: `const provincesResult = Provinces.generate(pack, config.provinces, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/provinces-generator.js`
- **Function Called**: `generate(pack, config.provinces, Utils)`
- **Data Created**: Provincial subdivisions
### Step 47: Define Burg Features
- **Line 111**: `const burgFeaturesResult = BurgsAndStates.defineBurgFeatures(undefined, pack, Utils);`
- **Function Called**: `BurgsAndStates.defineBurgFeatures()`
- **Data Created**: Detailed settlement features
### Step 48: Specify Rivers
- **Line 114**: `const specifiedRiversResult = Rivers.specify(pack, { Names }, Utils);`
- **Function Called**: `Rivers.specify()`
- **Data Created**: Named and detailed river information
### Step 49: Specify Features
- **Line 117**: `const specifiedFeaturesResult = Features.specify(pack, grid, { Lakes });`
- **Function Called**: `Features.specify()`
- **Data Created**: Detailed geographic feature information
### Step 50: Initialize Notes Array
- **Line 121**: `const notes = [];`
- **Data Created**: Notes array for modules requiring annotation
### Step 51: Generate Military
- **Line 123**: `const militaryResult = Military.generate(pack, config.military, Utils, notes);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/military-generator.js`
- **Function Called**: `generate(pack, config.military, Utils, notes)`
- **Data Created**: Military units and fortifications
### Step 52: Generate Markers
- **Line 126**: `const markersResult = Markers.generateMarkers(pack, config.markers, Utils);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/markers-generator.js`
- **Function Called**: `generateMarkers(pack, config.markers, Utils)`
- **Data Created**: Map markers and labels
### Step 53: Generate Zones
- **Line 129**: `const zonesResult = Zones.generate(pack, notes, Utils, config.zones);`
- **File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/modules/zones-generator.js`
- **Function Called**: `generate(pack, notes, Utils, config.zones)`
- **Data Created**: Special zones and areas
## Phase 16: Generation Completion
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/engine/main.js`
### Step 54: Log Performance Timing
- **Line 133**: `WARN && console.warn(\`TOTAL GENERATION TIME: ${Utils.rn((performance.now() - timeStart) / 1000, 2)}s\`);`
- **Action**: Performance timing logged if WARN debug flag is true
### Step 55: Close Console Group
- **Line 134**: `INFO && console.groupEnd("Generated Map " + seed);`
- **Action**: Console group ended if INFO debug flag is true
### Step 56: Return Generated Map Data
- **Line 137**: `return { seed, grid, pack, mapCoordinates };`
- **Data Returned**: Complete map data object containing:
- `seed`: Generation seed
- `grid`: Coarse Voronoi mesh with basic geographic data
- `pack`: Refined mesh with all generated features
- `mapCoordinates`: Geographic coordinate system
## Phase 17: Return to Viewer
**File**: `/home/user/Fantasy-Map-Generator/procedural/src/viewer/main.js`
### Step 57: Log Map Generation Complete
- **Line 72**: `console.log("Engine finished. Map data generated:", mapData);`
- **Data Received**: Complete `mapData` object from engine
- **Objects Available**: `{ seed, grid, pack, mapCoordinates }`
### Step 58: Render Map (Currently Commented Out)
- **Line 74**: `// renderMap(mapData);` (commented out)
- **Expected Next Step**: Rendering system would take the mapData and create visual representation
- **Current State**: Generation complete, awaiting rendering implementation
## Summary of Data Flow
### Key Data Transformations:
1. **UI → Configuration**: HTML form values → structured config object
2. **Configuration → Grid**: Config parameters → Voronoi mesh
3. **Grid → Heightmap**: Mesh structure → elevation data
4. **Grid → Features**: Heights → land/water classification
5. **Grid → Pack**: Coarse mesh → refined mesh
6. **Pack → Biomes**: Climate data → biome assignments
7. **Pack → Cultures**: Suitability → cultural territories
8. **Pack → Complete Map**: Sequential module processing → final map data
### Module Interaction Pattern:
- Each module receives: `(pack/grid, config_section, Utils, dependencies)`
- Each module returns: New data to merge into pack/grid
- Modules are stateless and pure (no side effects beyond returned data)
- Sequential processing builds up complexity from basic terrain to complete civilization
### Common Data Mismatches:
1. **Modules expecting properties that don't exist yet** (e.g., expecting `cells.culture` before Cultures module runs)
2. **Config sections missing expected fields** (validation tries to fix this)
3. **Pack/Grid structure differences** (pack is refined version of grid)
4. **Module dependencies** (Rivers needs Lakes, Cultures needs Names)
This trace shows the complete execution flow from button click to final map data generation, with each step clearly identifying the files, functions, data transformations, and object relationships involved in the Fantasy Map Generator's procedural generation process.

View file

@ -0,0 +1,52 @@
Looking more carefully at the code flow:
1. findGridCell vs Voronoi Cell Indices
The key insight is that findGridCell in graphUtils.js:100-105 is used for coordinate-to-grid mapping, but the
actual iteration in heightmap-generator.js happens over different indices.
2. Actual Iteration Patterns
In heightmap-generator.js, the problematic iterations are:
- Line 165: for (const c of grid.cells.c[q]) - but q comes from queue.shift() where queue starts with [start]
- Line 209: grid.cells.c[q].forEach(...) - same pattern
- Line 268: grid.cells.c[cur].forEach(...) - cur is tracked through valid cell traversal
- Line 516: grid.cells.c[i].forEach(...) - i is the map index over heights
3. The Critical Constraint
Looking at voronoi.js:22-27, cells.c[p] is only populated when:
if (p < this.pointsN && !this.cells.c[p]) {
// populate cells.c[p]
}
4. Why No Errors Occur
The code works because:
1. heights array length matches valid cells: In the original ../modules/heightmap-generator.js:11, heights is created with length:
points.length, which equals pointsN
```javascript
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;
};
```
1. Queue propagation stays within bounds: When iterating grid.cells.c[q], the q values come from:
- Initial valid starting points from findGridCell
- Subsequent values from grid.cells.c[previous_q] which only contains valid adjacent cell indices
2. Voronoi adjacency constraint: The cells.c[p] arrays only contain indices of valid neighboring cells (<
pointsN), so the iteration naturally stays within the populated sparse array bounds
3. Map iteration bounds: In line 468's heights.map((h, i) => ...), i ranges from 0 to heights.length-1, which
equals pointsN-1, so grid.cells.c[i] is always within the populated range.
The sparse array works because the algorithm's traversal patterns are naturally constrained to only access
indices that were populated during Voronoi construction.
## TASKS
1. analyze ../modules heightmap-generator.js and src/engine/modules/heightmap-generator.js to determine why the logic has deviated. Produce docs/HEIGHTMAP_ASSESSMENT.md with findings and recommendations

View file

@ -36,7 +36,7 @@
<input type="number" id="mapHeightInput" value="1080" min="100" max="8192" />
</div>
<!-- Graph Settings -->
<!-- Graph Settings (removed by Barrulus)
<div class="config-section">
<h3>Graph Settings</h3>
<label for="pointsInput">Cells:</label>
@ -54,6 +54,7 @@
<option value="circle">Circle</option>
</select>
</div>
-->
<!-- Heightmap Settings -->
<div class="config-section">
@ -72,7 +73,7 @@
</select>
</div>
<!-- Temperature Settings -->
<!-- Temperature Settings removed by Barrulus
<div class="config-section">
<h3>Temperature</h3>
<label for="temperatureEquatorOutput">Equator:</label>
@ -96,8 +97,9 @@
<label for="temperatureBase">Base Temp:</label>
<input type="number" id="temperatureBase" value="25" />
</div>
-->
<!-- Precipitation Settings -->
<!-- Precipitation Settings removed by Barrulus
<div class="config-section">
<h3>Precipitation</h3>
<label for="precInput">Precipitation:</label>
@ -106,8 +108,9 @@
<label for="moisture">Moisture:</label>
<input type="number" id="moisture" value="1" min="0.1" max="2" step="0.1" />
</div>
-->
<!-- Map Settings -->
<!-- Map Settings removed by Barrulus
<div class="config-section">
<h3>Map Settings</h3>
<label for="coordinatesSize">Coordinate Size:</label>
@ -116,26 +119,30 @@
<label for="latitude">Latitude:</label>
<input type="number" id="latitude" value="0" min="-90" max="90" />
</div>
-->
<!-- Lakes Settings -->
<!-- Lakes Settings removed by Barrulus
<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 -->
<!-- Rivers Settings removed by Barrulus
<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 -->
<!-- Ocean Layers removed by Barrulus
<div class="config-section">
<h3>Ocean</h3>
<div id="oceanLayers" layers="-1,-2,-3"></div>
</div>
-->
<!-- Cultures Settings -->
<div class="config-section">
@ -154,7 +161,7 @@
<option value="random" data-max="25">Random</option>
<option value="all-world" data-max="20">All World</option>
</select>
<!-- removed by Barrulus
<label for="emblemShape">Emblem Shape:</label>
<select id="emblemShape">
<option value="random">Random</option>
@ -188,6 +195,7 @@
<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 -->
@ -196,6 +204,7 @@
<label for="statesNumber">Number of States:</label>
<input type="number" id="statesNumber" value="15" min="0" max="999" />
<!-- removed by Barrulus
<label for="manorsInput">Number of Towns:</label>
<input type="number" id="manorsInput" value="1000" min="0" max="10000" title="1000 = auto-calculate" />
@ -207,6 +216,7 @@
<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 -->
@ -216,14 +226,15 @@
<input type="number" id="religionsNumber" value="5" min="0" max="99" />
</div>
<!-- Provinces Settings -->
<!-- Provinces Settings removed by Barrulus
<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 -->
<!-- Military Settings removed by Barrulus
<div class="config-section">
<h3>Military</h3>
<label for="year">Year:</label>
@ -235,19 +246,20 @@
<label for="era">Era:</label>
<input type="text" id="era" value="Anno Domini" />
</div>
-->
<!-- Zones Settings -->
<!-- Zones Settings removed by Barrulus
<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>

View file

@ -1,12 +1,12 @@
// main.js (Viewer Entry Point)
import './style.css';
import { generateMap } from './src/engine/main.js'; // Import from our future engine
import { generate } from './src/engine/main.js';
console.log("FMG Viewer Initialized!");
// Example of how you will eventually use the engine
document.addEventListener('DOMContentLoaded', () => {
const generateButton = document.getElementById('generateMapButton'); // Assuming you have a button with this ID
const generateButton = document.getElementById('generateButton'); // Assuming you have a button with this ID
generateButton.addEventListener('click', () => {
console.log("Generating map...");
@ -19,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => {
};
// 2. Call the engine
const mapData = generateMap(config);
const mapData = generate(config);
console.log("Map data generated by engine:", mapData);

View file

@ -19,6 +19,7 @@ import * as Routes from "./modules/routes-generator.js";
import * as Zones from "./modules/zones-generator.js";
import * as voronoi from "./modules/voronoi.js";
import * as Utils from "./utils/index.js";
import * as graphUtils from "./utils/graphUtils.js"
// Import the new utility modules
import * as Graph from "./utils/graph.js";
@ -30,7 +31,7 @@ import * as Cell from "./utils/cell.js";
* @param {object} config - A comprehensive configuration object.
* @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }.
*/
export function generate(config) {
export async function generate(config) {
const timeStart = performance.now();
// CORRECT: Get debug flags (values) from the config object.
@ -44,11 +45,11 @@ export function generate(config) {
INFO && console.group("Generating Map with Seed: " + seed);
// 2. Pass the 'graph' section of the config to the new graph utilities
let grid = Graph.generateGrid(config.graph);
// 2. Pass the entire config to generateGrid (it needs graph and debug sections)
let grid = graphUtils.generateGrid(config);
// --- Heightmap and Features (assumed to be already modular) ---
grid.cells.h = Heightmap.generate(grid, config.heightmap, Utils);
grid.cells.h = await Heightmap.generate(grid, config, Utils);
grid = Features.markupGrid(grid, config, Utils);
// 3. Pass 'map' and 'lakes' configs to the new geography utilities
@ -64,30 +65,30 @@ export function generate(config) {
// --- Pack Generation ---
let pack = Graph.reGraph(grid, Utils);
pack = Features.markupPack(pack, config, Utils, { Lakes });
pack = Features.markupPack(pack, grid, config, Utils, { Lakes });
// --- River Generation ---
const riverResult = Rivers.generate(pack, grid, config.rivers, Utils, { Lakes, Names });
const riverResult = Rivers.generate(pack, grid, config, Utils, { Lakes, Names });
pack = riverResult.pack;
// --- Biome and Population ---
const { biome } = Biomes.define(pack, grid, config.biomes, Utils);
const { biome } = Biomes.define(pack, grid, config, Utils);
pack.cells.biome = biome;
// 5. Call the new cell ranking utility
const { s, pop } = Cell.rankCells(pack, Utils, { biomesData: Biomes.getDefault() });
const { s, pop } = Cell.rankCells(pack, grid, config, 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 });
const culturesResult = Cultures.generate(pack, grid, config, 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 });
const burgsAndStatesResult = BurgsAndStates.generate(pack, grid, config.burgs, Utils);
pack = {
...pack,
burgs: burgsAndStatesResult.burgs,
@ -96,34 +97,37 @@ export function generate(config) {
pack.cells.burg = burgsAndStatesResult.burg;
pack.cells.state = burgsAndStatesResult.state;
const routesResult = Routes.generate(pack, Utils);
const routesResult = Routes.generate(pack, grid, Utils, []);
pack = { ...pack, ...routesResult }; // Merge new routes data
const religionsResult = Religions.generate(pack, config.religions, Utils, { Names, BurgsAndStates });
const religionsResult = Religions.generate(pack, grid, config.religions, Utils);
pack = { ...pack, ...religionsResult }; // Merge new religions data
const stateFormsResult = BurgsAndStates.defineStateForms(pack, Utils, { Names });
const stateFormsResult = BurgsAndStates.defineStateForms(undefined, pack, Utils);
pack = { ...pack, ...stateFormsResult }; // Merge updated state forms
const provincesResult = Provinces.generate(pack, config.provinces, Utils, { BurgsAndStates, Names, COA });
const provincesResult = Provinces.generate(pack, config.provinces, Utils);
pack = { ...pack, ...provincesResult }; // Merge new provinces data
const burgFeaturesResult = BurgsAndStates.defineBurgFeatures(pack, Utils);
const burgFeaturesResult = BurgsAndStates.defineBurgFeatures(undefined, pack, Utils);
pack = { ...pack, ...burgFeaturesResult }; // Merge updated burg features
const specifiedRiversResult = Rivers.specify(pack, Utils, { Names });
const specifiedRiversResult = Rivers.specify(pack, { Names }, Utils);
pack = { ...pack, ...specifiedRiversResult }; // Merge specified river data
const specifiedFeaturesResult = Features.specify(pack, grid, Utils, { Lakes });
const specifiedFeaturesResult = Features.specify(pack, grid, { Lakes });
pack = { ...pack, ...specifiedFeaturesResult }; // Merge specified feature data
const militaryResult = Military.generate(pack, config.military, Utils, { Names });
// Initialize notes array for modules that require it
const notes = [];
const militaryResult = Military.generate(pack, config.military, Utils, notes);
pack = { ...pack, ...militaryResult }; // Merge new military data
const markersResult = Markers.generate(pack, config.markers, Utils);
const markersResult = Markers.generateMarkers(pack, config.markers, Utils);
pack = { ...pack, ...markersResult }; // Merge new markers data
const zonesResult = Zones.generate(pack, config.zones, Utils);
const zonesResult = Zones.generate(pack, notes, Utils, config.zones);
pack = { ...pack, ...zonesResult }; // Merge new zones data

View file

@ -75,8 +75,34 @@ export const getDefault = () => {
return {i: Array.from({length: name.length}, (_, i) => i), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
};
// assign biome id for each cell
/**
* Assign biome id for each cell
*
* REQUIRES:
* - pack.cells.h (heights from heightmap processing)
* - grid.cells.temp (temperature from geography module)
* - grid.cells.prec (precipitation from geography module)
* - pack.cells.g (grid reference from pack generation)
* - config.debug (debug configuration)
*
* PROVIDES:
* - pack.cells.biome (biome assignments for each cell)
*/
export function define(pack, grid, config, utils) {
// Check required properties exist
if (!pack.cells.h) {
throw new Error("Biomes module requires pack.cells.h (heights) from heightmap processing");
}
if (!grid.cells.temp || !grid.cells.prec) {
throw new Error("Biomes module requires grid.cells.temp and grid.cells.prec from geography module");
}
if (!pack.cells.g) {
throw new Error("Biomes module requires pack.cells.g (grid reference) from pack generation");
}
if (!config.debug) {
throw new Error("Biomes module requires config.debug section");
}
const { d3, rn} = utils;
const { TIME } = config.debug;
TIME && console.time("defineBiomes");

View file

@ -1,6 +1,34 @@
"use strict";
/**
* Generates burgs (settlements) and states (political entities)
*
* REQUIRES:
* - pack.cells.culture (from cultures module)
* - pack.cells.s (from cell ranking)
* - pack.cultures (from cultures module)
* - config.statesNumber (number of states to generate)
*
* PROVIDES:
* - pack.burgs (burgs array)
* - pack.states (states array)
* - pack.cells.burg (burg assignments)
* - pack.cells.state (state assignments)
*/
export const generate = (pack, grid, config, utils) => {
// Check required properties exist
if (!pack.cells.culture) {
throw new Error("BurgsAndStates module requires cells.culture from Cultures module");
}
if (!pack.cells.s) {
throw new Error("BurgsAndStates module requires cells.s (suitability) from Cell ranking");
}
if (!pack.cultures) {
throw new Error("BurgsAndStates module requires pack.cultures from Cultures module");
}
if (!config.statesNumber) {
throw new Error("BurgsAndStates module requires config.statesNumber");
}
const {cells, cultures} = pack;
const n = cells.i.length;

View file

@ -1,16 +1,37 @@
"use strict";
export const generate = function (pack, grid, config, utils) {
/**
* Generates cultures (races, language zones) for the map
*
* REQUIRES:
* - pack.cells.s (suitability from cell ranking)
* - config.culturesInput (number of cultures to generate)
* - config.culturesInSetNumber (max cultures for culture set)
*
* PROVIDES:
* - pack.cells.culture (culture assignments for each cell)
* - pack.cultures (cultures array)
*/
export const generate = function (pack, grid, config, utils, modules) {
// Check required properties exist
if (!pack.cells.s) {
throw new Error("Cultures module requires cells.s (suitability) from Cell ranking");
}
if (!config.cultures || !config.cultures.culturesInput || !config.cultures.culturesInSetNumber) {
throw new Error("Cultures module requires config.cultures.culturesInput and config.cultures.culturesInSetNumber");
}
const { WARN, ERROR, rand, rn, P, minmax, biased, rw, abbreviate } = utils;
const { TIME } = config.debug;
const { Names } = modules;
TIME && console.time("generateCultures");
const cells = pack.cells;
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
const culturesInputNumber = config.culturesInput;
const culturesInSetNumber = config.culturesInSetNumber;
const culturesInputNumber = config.cultures.culturesInput;
const culturesInSetNumber = config.cultures.culturesInSetNumber;
let count = Math.min(culturesInputNumber, culturesInSetNumber);
const populated = cells.i.filter(i => cells.s[i]); // populated cells
@ -37,7 +58,7 @@ export const generate = function (pack, grid, config, utils) {
}
}
const cultures = selectCultures(count, config, pack, utils);
const cultures = selectCultures(count, config, pack, grid, utils);
const centers = utils.d3.quadtree();
const colors = getColors(count, utils);
const emblemShape = config.emblemShape;
@ -83,9 +104,9 @@ export const generate = function (pack, grid, config, utils) {
cultures.unshift({name: "Wildlands", i: 0, base: 1, origins: [null], shield: "round"});
// make sure all bases exist in nameBases
if (!utils.nameBases.length) {
if (!utils.nameBases || !utils.nameBases.length) {
ERROR && console.error("Name base is empty, default nameBases will be applied");
utils.nameBases = utils.Names.getNameBases();
utils.nameBases = Names.getNameBases();
}
cultures.forEach(c => (c.base = c.base % utils.nameBases.length));
@ -119,8 +140,8 @@ function placeCenter(sortingFn, populated, cultureIds, centers, cells, config, u
return cellId;
}
function selectCultures(culturesNumber, config, pack, utils) {
let defaultCultures = getDefault(culturesNumber, config, pack, utils);
function selectCultures(culturesNumber, config, pack, grid, utils) {
let defaultCultures = getDefault(culturesNumber, config, pack, grid, utils);
const cultures = [];
pack.cultures?.forEach(function (culture) {
@ -220,14 +241,14 @@ export const add = function (center, pack, config, utils) {
return newCulture;
};
export const getDefault = function (count, config, pack, utils) {
export const getDefault = function (count, config, pack, grid, utils) {
// generic sorting functions
const cells = pack.cells,
s = cells.s,
sMax = utils.d3.max(s),
t = cells.t,
h = cells.h,
temp = utils.grid.cells.temp;
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);

View file

@ -8,7 +8,7 @@ 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}) {
function markup({distanceField, neighbors, start, increment, limit = 127}) {
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
marked = 0;
const prevDistance = distance - increment;
@ -24,8 +24,31 @@ 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
*
* REQUIRES:
* - grid.cells.h (heights from heightmap generation)
* - grid.cells.c (cell neighbors from grid generation)
* - config.debug (debug configuration)
*
* PROVIDES:
* - grid.cells.f (feature assignments)
* - grid.cells.t (distance field)
* - grid.features (features array)
*/
export function markupGrid(grid, config, utils) {
// Check required properties exist
if (!grid.cells.h) {
throw new Error("Features module requires grid.cells.h (heights) from heightmap generation");
}
if (!grid.cells.c) {
throw new Error("Features module requires grid.cells.c (neighbors) from grid generation");
}
if (!config.debug) {
throw new Error("Features module requires config.debug section");
}
const {rn} = utils;
const { TIME } = config.debug;
@ -86,8 +109,31 @@ export function markupGrid(grid, config, utils) {
return updatedGrid;
}
// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
/**
* Mark Pack features (ocean, lakes, islands), calculate distance field and add properties
*
* REQUIRES:
* - pack.cells.h (heights from pack generation)
* - pack.cells.c (cell neighbors from pack generation)
* - grid.features (features from grid markup)
*
* PROVIDES:
* - pack.cells.f (feature assignments)
* - pack.cells.t (distance field)
* - pack.features (features array)
*/
export function markupPack(pack, grid, config, utils, modules) {
// Check required properties exist
if (!pack.cells.h) {
throw new Error("Features markupPack requires pack.cells.h (heights) from pack generation");
}
if (!pack.cells.c) {
throw new Error("Features markupPack requires pack.cells.c (neighbors) from pack generation");
}
if (!grid.features) {
throw new Error("Features markupPack requires grid.features from grid markup");
}
const {TIME} = config;
const {isLand, isWater, dist2, rn, clipPoly, unique, createTypedArray, connectVertices} = utils;
const {Lakes} = modules;
@ -111,7 +157,7 @@ export function markupPack(pack, grid, config, utils, modules) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell);
const land = isLand(firstCell, pack);
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
let totalCells = 1; // count cells in a feature
@ -121,7 +167,7 @@ export function markupPack(pack, grid, config, utils, modules) {
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = isLand(neighborId);
const isNeibLand = isLand(neighborId, pack);
if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
@ -166,7 +212,7 @@ export function markupPack(pack, grid, config, utils, modules) {
return updatedPack;
function defineHaven(cellId) {
const waterCells = neighbors[cellId].filter(isWater);
const waterCells = neighbors[cellId].filter(i => isWater(i, pack));
const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
@ -177,7 +223,7 @@ export function markupPack(pack, grid, config, utils, modules) {
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 points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]), config);
const area = d3.polygonArea(points); // feature perimiter area
const absArea = Math.abs(rn(area));
@ -194,8 +240,8 @@ export function markupPack(pack, grid, config, utils, modules) {
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);
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(i => isLand(i, pack))).flat());
feature.height = Lakes.getHeight(feature, pack, utils);
}
return feature;

View file

@ -1,73 +1,73 @@
"use strict";
export async function generate(graph, config, utils) {
const { aleaPRNG, heightmapTemplates } = utils;
const { TIME } = config.debug;
const { templateId, seed } = config;
export function fromTemplate(grid, templateId, config, utils) {
const { heightmapTemplates, aleaPRNG } = utils;
const templateString = heightmapTemplates[templateId]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${templateId}. Steps: ${steps}`);
const { cellsDesired, cells, points } = grid;
let heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
const blobPower = getBlobPower(cellsDesired);
const linePower = getLinePower(cellsDesired);
// Set up PRNG if seed is provided
if (config.seed !== undefined) {
Math.random = aleaPRNG(config.seed);
}
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${templateId}. Step: ${elements}`);
heights = addStep(heights, grid, blobPower, linePower, utils, ...elements);
}
return heights;
}
export async function fromPrecreated(grid, imageId, config, utils) {
// This function requires browser-specific Canvas API and Image loading
// It should be handled by the viewer layer, not the headless engine
throw new Error("fromPrecreated requires browser environment - should be handled by viewer layer");
}
export async function generate(grid, config, utils) {
const { TIME, aleaPRNG } = utils;
TIME && console.time("defineHeightmap");
const isTemplate = templateId in heightmapTemplates;
const heights = isTemplate
? fromTemplate(graph, templateId, config, utils)
: await fromPrecreated(graph, templateId, config, utils);
const templateId = config.heightmap.templateId;
// Set up PRNG if seed is provided
if (config.seed !== undefined) {
Math.random = aleaPRNG(config.seed);
}
const { heightmapTemplates } = utils;
const isTemplate = templateId in heightmapTemplates;
if (!isTemplate) {
throw new Error(`Template "${templateId}" not found. Available templates: ${Object.keys(heightmapTemplates).join(', ')}`);
}
const heights = fromTemplate(grid, templateId, config, utils);
TIME && console.timeEnd("defineHeightmap");
return heights;
}
// Placeholder function for processing precreated heightmaps
// This will need further refactoring to work headlessly (see heightmap-generator_render.md)
export async function fromPrecreated(graph, id, config, utils) {
// TODO: Implement headless image processing
// This function currently requires DOM/Canvas which was removed
// Future implementation will need:
// - utils.loadImage() function to load PNG files headlessly
// - Image processing library (e.g., canvas package for Node.js)
// - getHeightsFromImageData() refactored for headless operation
throw new Error(`fromPrecreated not yet implemented for headless operation. Template ID: ${id}`);
}
function addStep(heights, grid, blobPower, linePower, utils, tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(heights, grid, blobPower, utils, a2, a3, a4, a5);
if (tool === "Pit") return addPit(heights, grid, blobPower, utils, a2, a3, a4, a5);
if (tool === "Range") return addRange(heights, grid, linePower, utils, a2, a3, a4, a5);
if (tool === "Trough") return addTrough(heights, grid, linePower, utils, a2, a3, a4, a5);
if (tool === "Strait") return addStrait(heights, grid, utils, a2, a3);
if (tool === "Mask") return mask(heights, grid, a2);
if (tool === "Invert") return invert(heights, grid, 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, grid, utils, a2);
export function fromTemplate(graph, id, config, utils) {
const { heightmapTemplates } = utils;
const templateString = heightmapTemplates[id]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
let { heights, blobPower, linePower } = setGraph(graph, utils);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
heights = addStep(heights, graph, blobPower, linePower, config, utils, ...elements);
}
return heights;
}
function setGraph(graph, utils) {
const { createTypedArray } = utils;
const { cellsDesired, cells, points } = graph;
const heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
const blobPower = getBlobPower(cellsDesired);
const linePower = getLinePower(cellsDesired);
return { heights, blobPower, linePower };
}
function addStep(heights, graph, blobPower, linePower, config, utils, tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Pit") return addPit(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Range") return addRange(heights, graph, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Trough") return addTrough(heights, graph, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Strait") return addStrait(heights, graph, config, utils, a2, a3);
if (tool === "Mask") return mask(heights, graph, config, utils, a2);
if (tool === "Invert") return invert(heights, graph, config, utils, a2, a3);
if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
if (tool === "Multiply") return modify(heights, a3, 0, +a2, utils);
if (tool === "Smooth") return smooth(heights, graph, utils, a2);
return heights;
}
@ -110,12 +110,13 @@ function getLinePower(cells) {
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;
function addHill(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, findGridCell, lim } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneHill();
@ -131,34 +132,35 @@ export function addHill(heights, graph, blobPower, config, utils, count, height,
do {
const x = getPointInRange(rangeX, graphWidth, utils);
const y = getPointInRange(rangeY, graphHeight, utils);
start = findGridCell(x, y, graph);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] + h > 90 && limit < 50);
} while (newHeights[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]) {
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]));
newHeights = newHeights.map((h, i) => lim(h + change[i]));
}
return heights;
return newHeights;
}
export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, lim, findGridCell } = utils;
const { graphWidth, graphHeight } = config;
function addPit(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, findGridCell, lim } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOnePit();
@ -167,16 +169,15 @@ export function addPit(heights, graph, blobPower, config, utils, count, height,
function addOnePit() {
const used = new Uint8Array(heights.length);
let limit = 0,
start;
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);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
} while (newHeights[start] < 20 && limit < 50);
const queue = [start];
while (queue.length) {
@ -184,24 +185,26 @@ export function addPit(heights, graph, blobPower, config, utils, count, height,
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
if (h < 1) return;
graph.cells.c[q].forEach(function (c, i) {
grid.cells.c[q].forEach(function (c, i) {
if (used[c]) return;
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
newHeights[c] = lim(newHeights[c] - h * (Math.random() * 0.2 + 0.9));
used[c] = 1;
queue.push(c);
});
}
}
return heights;
return newHeights;
}
export function addRange(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, lim, findGridCell, d3 } = utils;
const { graphWidth, graphHeight } = config;
// fromCell, toCell are options cell ids
function addRange(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, findGridCell, lim, d3 } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneRange();
@ -217,10 +220,7 @@ export function addRange(heights, graph, linePower, config, utils, count, height
const startX = getPointInRange(rangeX, graphWidth, utils);
const startY = getPointInRange(rangeY, graphHeight, utils);
let dist = 0,
limit = 0,
endX,
endY;
let dist = 0, limit = 0, endX, endY;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
@ -229,8 +229,8 @@ export function addRange(heights, graph, linePower, config, utils, count, height
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCell = findGridCell(startX, startY, graph);
endCell = findGridCell(endX, endY, graph);
startCell = findGridCell(startX, startY, grid);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
@ -238,12 +238,12 @@ export function addRange(heights, graph, linePower, config, utils, count, height
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = graph.points;
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
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;
@ -261,18 +261,17 @@ export function addRange(heights, graph, linePower, config, utils, count, height
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
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));
newHeights[i] = lim(newHeights[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 => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -285,22 +284,23 @@ export function addRange(heights, graph, linePower, config, utils, count, height
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;
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
cur = min;
}
});
}
return heights;
return newHeights;
}
export function addTrough(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, lim, findGridCell, d3 } = utils;
const { graphWidth, graphHeight } = config;
function addTrough(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, findGridCell, lim, d3 } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneTrough();
@ -313,18 +313,13 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
if (rangeX && rangeY) {
// find start and end points
let limit = 0,
startX,
startY,
dist = 0,
endX,
endY;
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);
startCell = findGridCell(startX, startY, grid);
limit++;
} while (heights[startCell] < 20 && limit < 50);
} while (newHeights[startCell] < 20 && limit < 50);
limit = 0;
do {
@ -334,7 +329,7 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCell = findGridCell(endX, endY, graph);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
@ -342,12 +337,12 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = graph.points;
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
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;
@ -365,18 +360,17 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
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));
newHeights[i] = lim(newHeights[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 => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -389,25 +383,25 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
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;
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
cur = min;
}
});
}
return heights;
return newHeights;
}
export function addStrait(heights, graph, config, utils, width, direction = "vertical") {
function addStrait(heights, grid, utils, width, direction = "vertical") {
const { getNumberInRange, findGridCell, P } = utils;
const { graphWidth, graphHeight } = config;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
width = Math.min(getNumberInRange(width), graph.cellsX / 3);
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return heights;
let newHeights = new Uint8Array(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;
@ -419,18 +413,18 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
? 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);
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 = graph.points;
const p = grid.points;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
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) {
@ -449,12 +443,12 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
while (width > 0) {
const exp = 0.9 - step * width;
range.forEach(function (r) {
graph.cells.c[r].forEach(function (e) {
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;
newHeights[e] **= exp;
if (newHeights[e] > 100) newHeights[e] = 5;
});
});
range = query.slice();
@ -462,18 +456,16 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
width--;
}
return heights;
return newHeights;
}
export function modify(heights, range, add, mult, power, utils) {
const { lim } = utils;
heights = new Uint8Array(heights);
function modify(heights, range, add, mult, power) {
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
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 => {
return heights.map(h => {
if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
@ -481,33 +473,27 @@ export function modify(heights, range, add, mult, power, utils) {
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;
function smooth(heights, grid, utils, fr = 2, add = 0) {
const { d3, lim } = utils;
heights = new Uint8Array(heights);
heights = heights.map((h, i) => {
return heights.map((h, i) => {
const a = [h];
graph.cells.c[i].forEach(c => a.push(heights[c]));
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);
});
return heights;
}
export function mask(heights, graph, config, utils, power = 1) {
const { lim } = utils;
const { graphWidth, graphHeight } = config;
heights = new Uint8Array(heights);
function mask(heights, grid, power = 1) {
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
const fr = power ? Math.abs(power) : 1;
heights = heights.map((h, i) => {
const [x, y] = graph.points[i];
return 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
@ -515,19 +501,15 @@ export function mask(heights, graph, config, utils, power = 1) {
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;
function invert(heights, grid, count, axes) {
const { P } = { P: probability => Math.random() < probability };
if (!P(count)) return heights;
heights = new Uint8Array(heights);
const invertX = axes !== "y";
const invertY = axes !== "x";
const { cellsX, cellsY } = graph;
const { cellsX, cellsY } = grid;
const inverted = heights.map((h, i) => {
const x = i % cellsX;
@ -543,10 +525,9 @@ export function invert(heights, graph, config, utils, count, axes) {
}
function getPointInRange(range, length, utils) {
const { rand } = utils;
const { ERROR, rand } = utils;
if (typeof range !== "string") {
console.error("Range should be a string");
ERROR && console.error("Range should be a string");
return;
}
@ -554,3 +535,20 @@ function getPointInRange(range, length, utils) {
const max = range.split("-")[1] / 100 || min;
return rand(min * length, max * length);
}
function createTypedArray({ maxValue, length }) {
return new Uint8Array(length);
}
// Export utility functions for standalone use
export {
addHill,
addPit,
addRange,
addTrough,
addStrait,
smooth,
modify,
mask,
invert
};

View file

@ -0,0 +1,578 @@
"use strict";
/**
* Generates heightmap data for the grid
*
* REQUIRES:
* - grid.cells (from grid generation)
* - config.heightmap.templateId (heightmap configuration)
* - config.debug (debug configuration)
*
* PROVIDES:
* - grid.cells.h (height values for each cell)
*/
export async function generate(grid, config, utils) {
// Check required properties exist
if (!grid || !grid.cells) {
throw new Error("Heightmap module requires grid with cells structure");
}
if (!config.heightmap || !config.heightmap.templateId) {
throw new Error("Heightmap module requires config.heightmap.templateId");
}
if (!config.debug) {
throw new Error("Heightmap module requires config.debug section");
}
const { aleaPRNG, heightmapTemplates } = utils;
const { TIME } = config.debug;
const { templateId } = config.heightmap;
TIME && console.time("defineHeightmap");
const isTemplate = templateId in heightmapTemplates;
const heights = isTemplate
? fromTemplate(grid, templateId, config, utils)
: await fromPrecreated(grid, templateId, config, utils);
TIME && console.timeEnd("defineHeightmap");
return heights;
}
// Placeholder function for processing precreated heightmaps
// This will need further refactoring to work headlessly (see heightmap-generator_render.md)
export async function fromPrecreated(grid, id, config, utils) {
// TODO: Implement headless image processing
// This function currently requires DOM/Canvas which was removed
// Future implementation will need:
// - utils.loadImage() function to load PNG files headlessly
// - Image processing library (e.g., canvas package for Node.js)
// - getHeightsFromImageData() refactored for headless operation
throw new Error(`fromPrecreated not yet implemented for headless operation. Template ID: ${id}`);
}
export function fromTemplate(grid, 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 } = setGrid(grid, 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, grid, blobPower, linePower, config, utils, ...elements);
}
return heights;
}
function setGrid(grid, utils) {
const { createTypedArray } = utils;
const { cellsDesired, cells, points } = grid;
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, grid, blobPower, linePower, config, utils, tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(heights, grid, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Pit") return addPit(heights, grid, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Range") return addRange(heights, grid, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Trough") return addTrough(heights, grid, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Strait") return addStrait(heights, grid, config, utils, a2, a3);
if (tool === "Mask") return mask(heights, grid, config, utils, a2);
if (tool === "Invert") return invert(heights, grid, config, utils, a2, a3);
if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
if (tool === "Multiply") return modify(heights, a3, 0, +a2, utils);
if (tool === "Smooth") return smooth(heights, grid, 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, grid, 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, 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]));
}
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, 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);
});
}
}
return heights;
}
export function addRange(heights, grid, 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 = 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;
}
});
}
return heights;
}
export function addTrough(heights, grid, 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 = 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;
}
});
}
return heights;
}
export function addStrait(heights, grid, config, utils, width, direction = "vertical") {
const { getNumberInRange, findGridCell, P } = utils;
const { graphWidth, graphHeight } = config;
heights = new Uint8Array(heights);
width = Math.min(getNumberInRange(width), grid.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 = 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--;
}
return heights;
}
export function modify(heights, range, add, mult, power, utils) {
const { lim } = utils;
heights = new Uint8Array(heights);
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
const isLand = min === 20;
heights = heights.map(h => {
if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
return lim(h);
});
return heights;
}
export function smooth(heights, grid, utils, fr = 2, add = 0) {
const { lim, d3 } = utils;
heights = new Uint8Array(heights);
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);
});
return heights;
}
export function mask(heights, grid, 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] = 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);
});
return heights;
}
export function invert(heights, grid, 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 } = 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];
});
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

@ -0,0 +1,35 @@
# Config Properties for heightmap-generator.js
The refactored heightmap generator module requires the following config properties:
## Required Config Properties
### `heightmap.templateId` (string)
- **Original DOM source**: `byId("templateInput").value`
- **Purpose**: Specifies which heightmap template to use for generation
- **Example values**: `"continents"`, `"archipelago"`, `"volcano"`, `"atoll"`
- **Usage**: Determines the template key to look up in `heightmapTemplates[templateId]`
### `seed` (string|number, optional)
- **Original DOM source**: Global `seed` variable
- **Purpose**: Seed for the pseudorandom number generator to ensure reproducible heightmaps
- **Example values**: `"myseed123"`, `42`, `"continent_seed"`
- **Usage**: Passed to `aleaPRNG()` to initialize deterministic random generation
## Config Object Structure
```javascript
const config = {
heightmap: {
templateId: "continents" // Template ID to use
},
seed: "reproducible_seed", // Optional: PRNG seed for reproducibility
// ... other config properties for other modules
};
```
## Notes
- The `heightmap.templateId` property replaces the direct DOM access `byId("templateInput").value`
- The `seed` property ensures reproducible generation when the same seed is used
- Both properties should be validated by the calling code before passing to the generator

View file

@ -0,0 +1,35 @@
# External Dependencies for heightmap-generator.js
The refactored heightmap generator module requires the following external imports:
## From Utils Package
- `heightmapTemplates` - Object containing heightmap template definitions
- `aleaPRNG` - Pseudorandom number generator for reproducible results
- `getNumberInRange` - Utility to parse and generate numbers from range strings
- `findGridCell` - Function to find grid cell at given coordinates
- `lim` - Function to limit values to valid range (0-100)
- `d3` - D3.js utilities (specifically `d3.mean`, `d3.range`, `d3.scan`)
- `P` - Probability function (returns true/false based on probability)
- `rand` - Random number generator within range
- `ERROR` - Error logging flag
- `TIME` - Time logging flag
- `createTypedArray` - Factory function for creating typed arrays
- `minmax` - Utility to clamp values between min and max
## Internal Functions
- `getPointInRange` - Utility to generate coordinates within specified ranges (defined within the module)
## Template System
The module expects `heightmapTemplates` to be an object where each key is a template ID and each value has a `template` property containing newline-separated steps.
Example structure:
```javascript
const heightmapTemplates = {
"default": {
template: "Hill 5 50 10-30 10-30\nSmooth 2\nMask 1"
},
"archipelago": {
template: "Hill 20 30 0-100 0-100\nMask -0.5"
}
};
```

View file

@ -2,8 +2,31 @@
const LAKE_ELEVATION_DELTA = 0.1;
// check if lake can be potentially open (not in deep depression)
/**
* Check if lake can be potentially open (not in deep depression)
*
* REQUIRES:
* - pack.cells (pack cells data structure)
* - pack.features (features array)
* - pack.cells.c (cell neighbors)
* - pack.cells.f (cell features)
* - heights array (cell height values)
*
* PROVIDES:
* - Updated pack.features with closed property
*/
export function detectCloseLakes(pack, grid, heights, config) {
// Check required properties exist
if (!pack || !pack.cells || !pack.features) {
throw new Error("Lakes module requires pack with cells and features structures");
}
if (!pack.cells.c || !pack.cells.f) {
throw new Error("Lakes module requires pack.cells.c (neighbors) and pack.cells.f (features)");
}
if (!heights || !Array.isArray(heights)) {
throw new Error("Lakes module requires heights array");
}
const {cells, features} = pack;
const ELEVATION_LIMIT = config.lakeElevationLimit;

View file

@ -1,6 +1,32 @@
"use strict";
/**
* Generates military forces for states
*
* REQUIRES:
* - pack.cells.state (from BurgsAndStates module)
* - pack.states (from BurgsAndStates module)
* - pack.burgs (from BurgsAndStates module)
* - config.debug (debug configuration)
*
* PROVIDES:
* - pack.states[].military (military units for each state)
*/
export function generate(pack, config, utils, notes) {
// Check required properties exist
if (!pack.cells.state) {
throw new Error("Military module requires cells.state from BurgsAndStates module");
}
if (!pack.states) {
throw new Error("Military module requires pack.states from BurgsAndStates module");
}
if (!pack.burgs) {
throw new Error("Military module requires pack.burgs from BurgsAndStates module");
}
if (!config.debug) {
throw new Error("Military module requires config.debug section");
}
const { minmax, rn, ra, rand, gauss, si, nth, d3, populationRate, urbanization} = utils;
const { TIME } = config.debug;

View file

@ -28,6 +28,7 @@ export function generateOceanLayers(grid, config, utils) {
if (relaxed.length < 4) continue;
const points = clipPoly(
relaxed.map(v => vertices.p[v]),
config,
1
);
chains.push([t, points]);

View file

@ -9,7 +9,38 @@ const forms = {
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
};
/**
* Generates provinces for states
*
* REQUIRES:
* - pack.cells.state (from BurgsAndStates module)
* - pack.cells.burg (from BurgsAndStates module)
* - pack.states (from BurgsAndStates module)
* - pack.burgs (from BurgsAndStates module)
* - config.debug (debug configuration)
*
* PROVIDES:
* - pack.cells.province (province assignments)
* - pack.provinces (provinces array)
*/
export const generate = (pack, config, utils, regenerate = false, regenerateLockedStates = false) => {
// Check required properties exist
if (!pack.cells.state) {
throw new Error("Provinces module requires cells.state from BurgsAndStates module");
}
if (!pack.cells.burg) {
throw new Error("Provinces module requires cells.burg from BurgsAndStates module");
}
if (!pack.states) {
throw new Error("Provinces module requires pack.states from BurgsAndStates module");
}
if (!pack.burgs) {
throw new Error("Provinces module requires pack.burgs from BurgsAndStates module");
}
if (!config.debug) {
throw new Error("Provinces module requires config.debug section");
}
const {
generateSeed,
aleaPRNG,

View file

@ -451,7 +451,38 @@ const expansionismMap = {
Heresy: (utils) => utils.gauss(1, 0.5, 0, 5, 1)
};
/**
* Generates religions for the map
*
* REQUIRES:
* - pack.cells.culture (from cultures module)
* - pack.cells.state (from BurgsAndStates module)
* - pack.cultures (from cultures module)
* - config.religionsNumber (number of religions to generate)
* - config.debug (debug configuration)
*
* PROVIDES:
* - pack.cells.religion (religion assignments)
* - pack.religions (religions array)
*/
export function generate(pack, grid, config, utils) {
// Check required properties exist
if (!pack.cells.culture) {
throw new Error("Religions module requires cells.culture from Cultures module");
}
if (!pack.cells.state) {
throw new Error("Religions module requires cells.state from BurgsAndStates module");
}
if (!pack.cultures) {
throw new Error("Religions module requires pack.cultures from Cultures module");
}
if (!config.religionsNumber) {
throw new Error("Religions module requires config.religionsNumber");
}
if (!config.debug) {
throw new Error("Religions module requires config.debug section");
}
const { TIME } = config.debug;
TIME && console.time("generateReligions");
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];

View file

@ -1,6 +1,38 @@
"use strict";
/**
* Generates river systems for the map
*
* REQUIRES:
* - pack.cells.h (heights from heightmap processing)
* - pack.cells.t (distance field from features module)
* - pack.features (features from features module)
* - modules.Lakes (Lakes module dependency)
* - config.debug (debug configuration)
*
* PROVIDES:
* - pack.cells.fl (water flux)
* - pack.cells.r (river assignments)
* - pack.cells.conf (confluence data)
*/
export const generate = function (pack, grid, config, utils, modules, allowErosion = true) {
// Check required properties exist
if (!pack.cells.h) {
throw new Error("Rivers module requires pack.cells.h (heights) from heightmap processing");
}
if (!pack.cells.t) {
throw new Error("Rivers module requires pack.cells.t (distance field) from features module");
}
if (!pack.features) {
throw new Error("Rivers module requires pack.features from features module");
}
if (!modules.Lakes) {
throw new Error("Rivers module requires Lakes module dependency");
}
if (!config.debug) {
throw new Error("Rivers module requires config.debug section");
}
const { seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config;
const {rn, rw, each, round, d3, lineGen} = utils;
const {Lakes, Names} = modules;
@ -27,13 +59,13 @@ export const generate = function (pack, grid, config, utils, modules, allowErosi
let riverNext = 1; // first river id is 1
const h = alterHeights(pack, utils);
Lakes.detectCloseLakes(h);
Lakes.detectCloseLakes(pack, grid, h, config);
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();
Lakes.cleanupLakeData(pack);
let finalH = resolvedH;
if (allowErosion) {
@ -64,7 +96,7 @@ export const generate = function (pack, grid, config, utils, modules, allowErosi
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);
const lakeOutCells = Lakes.defineClimateData(pack, grid, h, config, utils);
land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation

View file

@ -10,7 +10,34 @@ const ROUTE_TYPE_MODIFIERS = {
default: 8 // far ocean
};
/**
* Generates routes (roads, trails, sea routes) connecting settlements
*
* REQUIRES:
* - pack.cells.burg (from BurgsAndStates module)
* - pack.burgs (from BurgsAndStates module)
* - pack.cells.h (heights from heightmap processing)
* - pack.cells.t (distance field from features module)
*
* PROVIDES:
* - pack.routes (routes array)
* - pack.cells.routes (route connections)
*/
export function generate(pack, grid, utils, lockedRoutes = []) {
// Check required properties exist
if (!pack.cells.burg) {
throw new Error("Routes module requires cells.burg from BurgsAndStates module");
}
if (!pack.burgs) {
throw new Error("Routes module requires pack.burgs from BurgsAndStates module");
}
if (!pack.cells.h) {
throw new Error("Routes module requires cells.h (heights) from heightmap processing");
}
if (!pack.cells.t) {
throw new Error("Routes module requires cells.t (distance field) from features module");
}
const { dist2, findPath, findCell, rn } = utils;
const { capitalsByFeature, burgsByFeature, portsByFeature } = sortBurgsByFeature(pack.burgs);

View file

@ -2,7 +2,7 @@
// FMG utils related to cell ranking and population
// calculate cell suitability and population based on various factors
function rankCells(pack, grid, utils, modules) {
function rankCells(pack, grid, config, utils, modules) {
const { normalize } = utils;
const { TIME } = config.debug;
const { biomesData } = modules;

View file

@ -1,14 +1,18 @@
"use strict";
import { polygonclip } from './lineclip.js';
// FMG helper functions
// clip polygon by graph bbox
function clipPoly(points, secure = 0) {
function clipPoly(points, config, secure = 0) {
if (points.length < 2) return points;
if (points.some(point => point === undefined)) {
ERROR && console.error("Undefined point in clipPoly", points);
console.error("Undefined point in clipPoly", points);
return points;
}
const graphWidth = config.graph.width || 1000;
const graphHeight = config.graph.height || 1000;
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}

View file

@ -5,17 +5,6 @@ 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) {
@ -38,11 +27,20 @@ function calculateVoronoi(points, boundary) {
const cells = voronoi.cells;
cells.i = createTypedArray({ maxValue: points.length, length: points.length }).map((_, i) => i);
// Ensure all cells have neighbor arrays initialized
for (let i = 0; i < points.length; i++) {
if (!cells.c[i]) cells.c[i] = [];
if (!cells.v[i]) cells.v[i] = [];
if (!cells.b[i]) cells.b[i] = 0;
}
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);

View file

@ -0,0 +1,332 @@
"use strict";
// FMG utils related to graph
//
import Delaunator from 'delaunator';
import { Voronoi } from '../modules/voronoi.js';
import { rn } from './numberUtils.js';
import { createTypedArray } from './arrayUtils.js';
import * as d3 from 'd3';
/**
* 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 }.
*/
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, config);
return { spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices };
}
// place random points to calculate Voronoi diagram
function placePoints(config) {
const { TIME } = config.debug || {};
const { width, height, cellsDesired } = config.graph;
TIME && console.time("placePoints");
const spacing = rn(Math.sqrt((width * height) / cellsDesired), 2); // spacing between points before jirrering
const boundary = getBoundaryPoints(width, height, spacing);
const points = getJitteredGrid(width, height, spacing); // points of jittered square grid
const cellsX = Math.floor((width + 0.5 * spacing - 1e-10) / spacing);
const cellsY = Math.floor((height + 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, config = {}) {
const { TIME } = config.debug || {};
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, grid) {
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, pack) {
return pack.cells.q.find(x, y, radius);
}
// return closest cell index
function findCell(x, y, radius = Infinity, pack) {
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, pack) {
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, pack) {
return pack.cells.v[i].map(v => pack.vertices.p[v]);
}
// get polygon points for initial cells knowing cell id
function getGridPolygon(i, grid) {
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, pack) {
return pack.cells.h[i] >= 20;
}
// filter water cells
function isWater(i, pack) {
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));
}
};
})();
// Export all functions
export {
generateGrid,
placePoints,
calculateVoronoi,
getBoundaryPoints,
getJitteredGrid,
findGridCell,
findGridAll,
find,
findCell,
findAll,
getPackPolygon,
getGridPolygon,
poissonDiscSampler,
isLand,
isWater
};

View file

@ -0,0 +1,168 @@
"use strict";
const heightmapTemplates = (function () {
const volcano = `Hill 1 90-100 44-56 40-60
Multiply 0.8 50-100 0 0
Range 1.5 30-55 45-55 40-60
Smooth 3 0 0 0
Hill 1.5 35-45 25-30 20-75
Hill 1 35-55 75-80 25-75
Hill 0.5 20-25 10-15 20-25
Mask 3 0 0 0`;
const highIsland = `Hill 1 90-100 65-75 47-53
Add 7 all 0 0
Hill 5-6 20-30 25-55 45-55
Range 1 40-50 45-55 45-55
Multiply 0.8 land 0 0
Mask 3 0 0 0
Smooth 2 0 0 0
Trough 2-3 20-30 20-30 20-30
Trough 2-3 20-30 60-80 70-80
Hill 1 10-15 60-60 50-50
Hill 1.5 13-16 15-20 20-75
Range 1.5 30-40 15-85 30-40
Range 1.5 30-40 15-85 60-70
Pit 3-5 10-30 15-85 20-80`;
const lowIsland = `Hill 1 90-99 60-80 45-55
Hill 1-2 20-30 10-30 10-90
Smooth 2 0 0 0
Hill 6-7 25-35 20-70 30-70
Range 1 40-50 45-55 45-55
Trough 2-3 20-30 15-85 20-30
Trough 2-3 20-30 15-85 70-80
Hill 1.5 10-15 5-15 20-80
Hill 1 10-15 85-95 70-80
Pit 5-7 15-25 15-85 20-80
Multiply 0.4 20-100 0 0
Mask 4 0 0 0`;
const continents = `Hill 1 80-85 60-80 40-60
Hill 1 80-85 20-30 40-60
Hill 6-7 15-30 25-75 15-85
Multiply 0.6 land 0 0
Hill 8-10 5-10 15-85 20-80
Range 1-2 30-60 5-15 25-75
Range 1-2 30-60 80-95 25-75
Range 0-3 30-60 80-90 20-80
Strait 2 vertical 0 0
Strait 1 vertical 0 0
Smooth 3 0 0 0
Trough 3-4 15-20 15-85 20-80
Trough 3-4 5-10 45-55 45-55
Pit 3-4 10-20 15-85 20-80
Mask 4 0 0 0`;
const archipelago = `Add 11 all 0 0
Range 2-3 40-60 20-80 20-80
Hill 5 15-20 10-90 30-70
Hill 2 10-15 10-30 20-80
Hill 2 10-15 60-90 20-80
Smooth 3 0 0 0
Trough 10 20-30 5-95 5-95
Strait 2 vertical 0 0
Strait 2 horizontal 0 0`;
const atoll = `Hill 1 75-80 50-60 45-55
Hill 1.5 30-50 25-75 30-70
Hill .5 30-50 25-35 30-70
Smooth 1 0 0 0
Multiply 0.2 25-100 0 0
Hill 0.5 10-20 50-55 48-52`;
const mediterranean = `Range 4-6 30-80 0-100 0-10
Range 4-6 30-80 0-100 90-100
Hill 6-8 30-50 10-90 0-5
Hill 6-8 30-50 10-90 95-100
Multiply 0.9 land 0 0
Mask -2 0 0 0
Smooth 1 0 0 0
Hill 2-3 30-70 0-5 20-80
Hill 2-3 30-70 95-100 20-80
Trough 3-6 40-50 0-100 0-10
Trough 3-6 40-50 0-100 90-100`;
const peninsula = `Range 2-3 20-35 40-50 0-15
Add 5 all 0 0
Hill 1 90-100 10-90 0-5
Add 13 all 0 0
Hill 3-4 3-5 5-95 80-100
Hill 1-2 3-5 5-95 40-60
Trough 5-6 10-25 5-95 5-95
Smooth 3 0 0 0
Invert 0.4 both 0 0`;
const pangea = `Hill 1-2 25-40 15-50 0-10
Hill 1-2 5-40 50-85 0-10
Hill 1-2 25-40 50-85 90-100
Hill 1-2 5-40 15-50 90-100
Hill 8-12 20-40 20-80 48-52
Smooth 2 0 0 0
Multiply 0.7 land 0 0
Trough 3-4 25-35 5-95 10-20
Trough 3-4 25-35 5-95 80-90
Range 5-6 30-40 10-90 35-65`;
const isthmus = `Hill 5-10 15-30 0-30 0-20
Hill 5-10 15-30 10-50 20-40
Hill 5-10 15-30 30-70 40-60
Hill 5-10 15-30 50-90 60-80
Hill 5-10 15-30 70-100 80-100
Smooth 2 0 0 0
Trough 4-8 15-30 0-30 0-20
Trough 4-8 15-30 10-50 20-40
Trough 4-8 15-30 30-70 40-60
Trough 4-8 15-30 50-90 60-80
Trough 4-8 15-30 70-100 80-100
Invert 0.25 x 0 0`;
const shattered = `Hill 8 35-40 15-85 30-70
Trough 10-20 40-50 5-95 5-95
Range 5-7 30-40 10-90 20-80
Pit 12-20 30-40 15-85 20-80`;
const taklamakan = `Hill 1-3 20-30 30-70 30-70
Hill 2-4 60-85 0-5 0-100
Hill 2-4 60-85 95-100 0-100
Hill 3-4 60-85 20-80 0-5
Hill 3-4 60-85 20-80 95-100
Smooth 3 0 0 0`;
const oldWorld = `Range 3 70 15-85 20-80
Hill 2-3 50-70 15-45 20-80
Hill 2-3 50-70 65-85 20-80
Hill 4-6 20-25 15-85 20-80
Multiply 0.5 land 0 0
Smooth 2 0 0 0
Range 3-4 20-50 15-35 20-45
Range 2-4 20-50 65-85 45-80
Strait 3-7 vertical 0 0
Trough 6-8 20-50 15-85 45-65
Pit 5-6 20-30 10-90 10-90`;
const fractious = `Hill 12-15 50-80 5-95 5-95
Mask -1.5 0 0 0
Mask 3 0 0 0
Add -20 30-100 0 0
Range 6-8 40-50 5-95 10-90`;
return {
volcano: {id: 0, name: "Volcano", template: volcano, probability: 3},
highIsland: {id: 1, name: "High Island", template: highIsland, probability: 19},
lowIsland: {id: 2, name: "Low Island", template: lowIsland, probability: 9},
continents: {id: 3, name: "Continents", template: continents, probability: 16},
archipelago: {id: 4, name: "Archipelago", template: archipelago, probability: 18},
atoll: {id: 5, name: "Atoll", template: atoll, probability: 1},
mediterranean: {id: 6, name: "Mediterranean", template: mediterranean, probability: 5},
peninsula: {id: 7, name: "Peninsula", template: peninsula, probability: 3},
pangea: {id: 8, name: "Pangea", template: pangea, probability: 5},
isthmus: {id: 9, name: "Isthmus", template: isthmus, probability: 2},
shattered: {id: 10, name: "Shattered", template: shattered, probability: 7},
taklamakan: {id: 11, name: "Taklamakan", template: taklamakan, probability: 1},
oldWorld: {id: 12, name: "Old World", template: oldWorld, probability: 8},
fractious: {id: 13, name: "Fractious", template: fractious, probability: 3}
};
})();
export { heightmapTemplates };

View file

@ -1,6 +1,8 @@
import "./polyfills.js";
import * as d3 from 'd3';
export { aleaPRNG } from './alea.js'
export { d3 };
export {
last,
unique,
@ -39,9 +41,25 @@ export {
} from "./debugUtils.js";
export { rollups, nest, dist2 } from "./functionUtils.js";
export {
generateGrid,
reGraph,
reGraph
} from "./graph.js";
export {
generateGrid,
placePoints,
calculateVoronoi,
getBoundaryPoints,
getJitteredGrid,
findGridCell,
findGridAll,
find,
findCell,
findAll,
getPackPolygon,
getGridPolygon,
poissonDiscSampler,
isLand,
isWater
} from "./graphUtils.js";
export {
removeParent,
getComposedPath,
@ -88,3 +106,4 @@ export {
export { convertTemperature, si, getInteger } from "./unitUtils.js";
export { simplify } from "./simplify.js";
export { lineclip } from "./lineclip.js";
export { heightmapTemplates } from "./heightmap-templates.js";

View file

@ -1,3 +1,3 @@
// lineclip by mourner, https://github.com/mapbox/lineclip
"use strict";function lineclip(t,e,n){var r,i,u,o,s,h=t.length,c=bitCode(t[0],e),f=[];for(n=n||[],r=1;r<h;r++){for(i=t[r-1],o=s=bitCode(u=t[r],e);;){if(!(c|o)){f.push(i),o!==s?(f.push(u),r<h-1&&(n.push(f),f=[])):r===h-1&&f.push(u);break}if(c&o)break;c?c=bitCode(i=intersect(i,u,c,e),e):o=bitCode(u=intersect(i,u,o,e),e)}c=s}return f.length&&n.push(f),n}function polygonclip(t,e,n=0){for(var r,i,u,o,s,h,c,f=1;f<=8;f*=2){for(r=[],u=!(bitCode(i=t[t.length-1],e)&f),s=0;s<t.length;s++){o=(c=!(bitCode(h=t[s],e)&f))!==u;var l=intersect(i,h,f,e);o&&r.push(l),n&&o&&r.push(l,l),c&&r.push(h),i=h,u=c}if(!(t=r).length)break}return r}function intersect(t,e,n,r){return 8&n?[t[0]+(e[0]-t[0])*(r[3]-t[1])/(e[1]-t[1]),r[3]]:4&n?[t[0]+(e[0]-t[0])*(r[1]-t[1])/(e[1]-t[1]),r[1]]:2&n?[r[2],t[1]+(e[1]-t[1])*(r[2]-t[0])/(e[0]-t[0])]:1&n?[r[0],t[1]+(e[1]-t[1])*(r[0]-t[0])/(e[0]-t[0])]:null}function bitCode(t,e){var n=0;return t[0]<e[0]?n|=1:t[0]>e[2]&&(n|=2),t[1]<e[1]?n|=4:t[1]>e[3]&&(n|=8),n}
export { lineclip };
export { lineclip, polygonclip };

View file

@ -78,6 +78,9 @@ export function validateConfig(config) {
// Validate logical constraints
validateLogicalConstraints(config, result);
// Validate required fields for modules
validateRequiredFields(config, result);
// Set valid flag based on errors
result.valid = result.errors.length === 0;
@ -305,6 +308,49 @@ function validateLogicalConstraints(config, result) {
}
}
/**
* Validate required fields for modules
*/
function validateRequiredFields(config, result) {
const requiredFields = {
'cultures.culturesInSetNumber': (config) => {
// Ensure this field exists based on culturesSet
const maxCultures = getCultureSetMax(config.cultures.culturesSet);
return maxCultures;
},
'rivers.cellsCount': (config) => {
// Ensure this matches the actual cell count
return config.graph.cellsDesired || 10000;
}
};
// Check each required field
Object.keys(requiredFields).forEach(fieldPath => {
const value = getNestedProperty(config, fieldPath);
if (value === undefined) {
const defaultValue = requiredFields[fieldPath](config);
result.warnings.push(`Missing field ${fieldPath}, would default to ${defaultValue}`);
}
});
}
/**
* Get maximum cultures for a culture set
*/
function getCultureSetMax(culturesSet) {
const sets = {
european: 25,
oriental: 20,
english: 15,
antique: 18,
highFantasy: 30,
darkFantasy: 25,
random: 50,
'all-world': 100
};
return sets[culturesSet] || 25;
}
/**
* Helper function to get nested property value
*/

View file

@ -29,7 +29,7 @@ import {
/**
* Handle map generation with validation
*/
function handleGenerateClick() {
async function handleGenerateClick() {
console.log("Building config from UI and calling engine...");
// Build configuration from current UI state
@ -67,7 +67,7 @@ function handleGenerateClick() {
}
// Single, clean call to the engine with validated config
const mapData = generateMapEngine(fixed);
const mapData = await generateMapEngine(fixed);
console.log("Engine finished. Map data generated:", mapData);
// The renderer will take over from here