mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
more porting work
This commit is contained in:
parent
37391c8e8b
commit
7f31969f50
38 changed files with 3673 additions and 463 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
.vscode
|
.vscode/
|
||||||
.idea
|
.idea/
|
||||||
|
.claude/
|
||||||
|
.obsidian/
|
||||||
/node_modules
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
/coverage
|
/coverage
|
||||||
|
|
|
||||||
112
procedural/CLAUDE.md
Normal file
112
procedural/CLAUDE.md
Normal 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
|
||||||
|
|
@ -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.
|
|
||||||
246
procedural/docs/CALL_PATTERN.md
Normal file
246
procedural/docs/CALL_PATTERN.md
Normal 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.
|
||||||
242
procedural/docs/Data_Mismatch_Task_Activity.md
Normal file
242
procedural/docs/Data_Mismatch_Task_Activity.md
Normal 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.
|
||||||
177
procedural/docs/Data_Mismatch_Tasks.md
Normal file
177
procedural/docs/Data_Mismatch_Tasks.md
Normal 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
|
||||||
366
procedural/docs/FMG Data Model.md
Normal file
366
procedural/docs/FMG Data Model.md
Normal 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]
|
||||||
|
```
|
||||||
226
procedural/docs/HEIGHTMAP_ASSESSMENT.md
Normal file
226
procedural/docs/HEIGHTMAP_ASSESSMENT.md
Normal 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.
|
||||||
460
procedural/docs/MAP_GENERATION_TRACE.md
Normal file
460
procedural/docs/MAP_GENERATION_TRACE.md
Normal 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.
|
||||||
52
procedural/docs/findGridCell.md
Normal file
52
procedural/docs/findGridCell.md
Normal 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
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<!-- Configuration Panel -->
|
<!-- Configuration Panel -->
|
||||||
<div id="optionsContainer">
|
<div id="optionsContainer">
|
||||||
<h2>Map Configuration</h2>
|
<h2>Map Configuration</h2>
|
||||||
|
|
||||||
<!-- Preset Selector -->
|
<!-- Preset Selector -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<label for="presetSelector">Preset:</label>
|
<label for="presetSelector">Preset:</label>
|
||||||
|
|
@ -28,32 +28,33 @@
|
||||||
<label for="optionsSeed">Seed:</label>
|
<label for="optionsSeed">Seed:</label>
|
||||||
<input type="text" id="optionsSeed" placeholder="Auto-generate" />
|
<input type="text" id="optionsSeed" placeholder="Auto-generate" />
|
||||||
<input type="text" id="seed" placeholder="Alternative seed input" />
|
<input type="text" id="seed" placeholder="Alternative seed input" />
|
||||||
|
|
||||||
<label for="mapWidthInput">Width:</label>
|
<label for="mapWidthInput">Width:</label>
|
||||||
<input type="number" id="mapWidthInput" value="1920" min="100" max="8192" />
|
<input type="number" id="mapWidthInput" value="1920" min="100" max="8192" />
|
||||||
|
|
||||||
<label for="mapHeightInput">Height:</label>
|
<label for="mapHeightInput">Height:</label>
|
||||||
<input type="number" id="mapHeightInput" value="1080" min="100" max="8192" />
|
<input type="number" id="mapHeightInput" value="1080" min="100" max="8192" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Graph Settings -->
|
<!-- Graph Settings (removed by Barrulus)
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Graph Settings</h3>
|
<h3>Graph Settings</h3>
|
||||||
<label for="pointsInput">Cells:</label>
|
<label for="pointsInput">Cells:</label>
|
||||||
<input type="number" id="pointsInput" value="10000" min="1000" max="100000" data-cells="10000" />
|
<input type="number" id="pointsInput" value="10000" min="1000" max="100000" data-cells="10000" />
|
||||||
|
|
||||||
<label for="cellsNumber">Cell Count:</label>
|
<label for="cellsNumber">Cell Count:</label>
|
||||||
<input type="number" id="cellsNumber" value="10000" readonly />
|
<input type="number" id="cellsNumber" value="10000" readonly />
|
||||||
|
|
||||||
<label for="pointsNumber">Points:</label>
|
<label for="pointsNumber">Points:</label>
|
||||||
<input type="number" id="pointsNumber" value="10000" readonly />
|
<input type="number" id="pointsNumber" value="10000" readonly />
|
||||||
|
|
||||||
<label for="boundary">Boundary:</label>
|
<label for="boundary">Boundary:</label>
|
||||||
<select id="boundary">
|
<select id="boundary">
|
||||||
<option value="box">Box</option>
|
<option value="box">Box</option>
|
||||||
<option value="circle">Circle</option>
|
<option value="circle">Circle</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Heightmap Settings -->
|
<!-- Heightmap Settings -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
|
@ -72,77 +73,83 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temperature Settings -->
|
<!-- Temperature Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Temperature</h3>
|
<h3>Temperature</h3>
|
||||||
<label for="temperatureEquatorOutput">Equator:</label>
|
<label for="temperatureEquatorOutput">Equator:</label>
|
||||||
<input type="number" id="temperatureEquatorOutput" value="30" />
|
<input type="number" id="temperatureEquatorOutput" value="30" />
|
||||||
|
|
||||||
<label for="temperatureNorthPoleOutput">North Pole:</label>
|
<label for="temperatureNorthPoleOutput">North Pole:</label>
|
||||||
<input type="number" id="temperatureNorthPoleOutput" value="-10" />
|
<input type="number" id="temperatureNorthPoleOutput" value="-10" />
|
||||||
|
|
||||||
<label for="temperatureSouthPoleOutput">South Pole:</label>
|
<label for="temperatureSouthPoleOutput">South Pole:</label>
|
||||||
<input type="number" id="temperatureSouthPoleOutput" value="-15" />
|
<input type="number" id="temperatureSouthPoleOutput" value="-15" />
|
||||||
|
|
||||||
<label for="heightExponentInput">Height Exponent:</label>
|
<label for="heightExponentInput">Height Exponent:</label>
|
||||||
<input type="number" id="heightExponentInput" value="1.8" min="0.5" max="5" step="0.1" />
|
<input type="number" id="heightExponentInput" value="1.8" min="0.5" max="5" step="0.1" />
|
||||||
|
|
||||||
<label for="temperatureScale">Scale:</label>
|
<label for="temperatureScale">Scale:</label>
|
||||||
<select id="temperatureScale">
|
<select id="temperatureScale">
|
||||||
<option value="C">Celsius</option>
|
<option value="C">Celsius</option>
|
||||||
<option value="F">Fahrenheit</option>
|
<option value="F">Fahrenheit</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="temperatureBase">Base Temp:</label>
|
<label for="temperatureBase">Base Temp:</label>
|
||||||
<input type="number" id="temperatureBase" value="25" />
|
<input type="number" id="temperatureBase" value="25" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Precipitation Settings -->
|
<!-- Precipitation Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Precipitation</h3>
|
<h3>Precipitation</h3>
|
||||||
<label for="precInput">Precipitation:</label>
|
<label for="precInput">Precipitation:</label>
|
||||||
<input type="number" id="precInput" value="100" />
|
<input type="number" id="precInput" value="100" />
|
||||||
|
|
||||||
<label for="moisture">Moisture:</label>
|
<label for="moisture">Moisture:</label>
|
||||||
<input type="number" id="moisture" value="1" min="0.1" max="2" step="0.1" />
|
<input type="number" id="moisture" value="1" min="0.1" max="2" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Map Settings -->
|
<!-- Map Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Map Settings</h3>
|
<h3>Map Settings</h3>
|
||||||
<label for="coordinatesSize">Coordinate Size:</label>
|
<label for="coordinatesSize">Coordinate Size:</label>
|
||||||
<input type="number" id="coordinatesSize" value="1" min="0.1" max="10" step="0.1" />
|
<input type="number" id="coordinatesSize" value="1" min="0.1" max="10" step="0.1" />
|
||||||
|
|
||||||
<label for="latitude">Latitude:</label>
|
<label for="latitude">Latitude:</label>
|
||||||
<input type="number" id="latitude" value="0" min="-90" max="90" />
|
<input type="number" id="latitude" value="0" min="-90" max="90" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Lakes Settings -->
|
<!-- Lakes Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Lakes</h3>
|
<h3>Lakes</h3>
|
||||||
<label for="lakeElevationLimitOutput">Elevation Limit:</label>
|
<label for="lakeElevationLimitOutput">Elevation Limit:</label>
|
||||||
<input type="number" id="lakeElevationLimitOutput" value="50" min="0" max="100" />
|
<input type="number" id="lakeElevationLimitOutput" value="50" min="0" max="100" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Rivers Settings -->
|
<!-- Rivers Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Rivers</h3>
|
<h3>Rivers</h3>
|
||||||
<label for="resolveDepressionsStepsOutput">Depression Steps:</label>
|
<label for="resolveDepressionsStepsOutput">Depression Steps:</label>
|
||||||
<input type="number" id="resolveDepressionsStepsOutput" value="1000" min="100" max="10000" />
|
<input type="number" id="resolveDepressionsStepsOutput" value="1000" min="100" max="10000" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Ocean Layers -->
|
<!-- Ocean Layers removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Ocean</h3>
|
<h3>Ocean</h3>
|
||||||
<div id="oceanLayers" layers="-1,-2,-3"></div>
|
<div id="oceanLayers" layers="-1,-2,-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Cultures Settings -->
|
<!-- Cultures Settings -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Cultures</h3>
|
<h3>Cultures</h3>
|
||||||
<label for="culturesInput">Number of Cultures:</label>
|
<label for="culturesInput">Number of Cultures:</label>
|
||||||
<input type="number" id="culturesInput" value="12" min="0" max="99" />
|
<input type="number" id="culturesInput" value="12" min="0" max="99" />
|
||||||
|
|
||||||
<label for="culturesSet">Culture Set:</label>
|
<label for="culturesSet">Culture Set:</label>
|
||||||
<select id="culturesSet">
|
<select id="culturesSet">
|
||||||
<option value="european" data-max="15">European</option>
|
<option value="european" data-max="15">European</option>
|
||||||
|
|
@ -154,7 +161,7 @@
|
||||||
<option value="random" data-max="25">Random</option>
|
<option value="random" data-max="25">Random</option>
|
||||||
<option value="all-world" data-max="20">All World</option>
|
<option value="all-world" data-max="20">All World</option>
|
||||||
</select>
|
</select>
|
||||||
|
<!-- removed by Barrulus
|
||||||
<label for="emblemShape">Emblem Shape:</label>
|
<label for="emblemShape">Emblem Shape:</label>
|
||||||
<select id="emblemShape">
|
<select id="emblemShape">
|
||||||
<option value="random">Random</option>
|
<option value="random">Random</option>
|
||||||
|
|
@ -185,9 +192,10 @@
|
||||||
<option value="banner">Banner</option>
|
<option value="banner">Banner</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="neutralRate">Neutral Rate:</label>
|
<label for="neutralRate">Neutral Rate:</label>
|
||||||
<input type="number" id="neutralRate" value="1" min="0.1" max="10" step="0.1" />
|
<input type="number" id="neutralRate" value="1" min="0.1" max="10" step="0.1" />
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- States & Burgs Settings -->
|
<!-- States & Burgs Settings -->
|
||||||
|
|
@ -195,18 +203,20 @@
|
||||||
<h3>States & Burgs</h3>
|
<h3>States & Burgs</h3>
|
||||||
<label for="statesNumber">Number of States:</label>
|
<label for="statesNumber">Number of States:</label>
|
||||||
<input type="number" id="statesNumber" value="15" min="0" max="999" />
|
<input type="number" id="statesNumber" value="15" min="0" max="999" />
|
||||||
|
|
||||||
|
<!-- removed by Barrulus
|
||||||
<label for="manorsInput">Number of Towns:</label>
|
<label for="manorsInput">Number of Towns:</label>
|
||||||
<input type="number" id="manorsInput" value="1000" min="0" max="10000" title="1000 = auto-calculate" />
|
<input type="number" id="manorsInput" value="1000" min="0" max="10000" title="1000 = auto-calculate" />
|
||||||
|
|
||||||
<label for="sizeVariety">Size Variety:</label>
|
<label for="sizeVariety">Size Variety:</label>
|
||||||
<input type="number" id="sizeVariety" value="1" min="0" max="5" step="0.1" />
|
<input type="number" id="sizeVariety" value="1" min="0" max="5" step="0.1" />
|
||||||
|
|
||||||
<label for="growthRate">Growth Rate:</label>
|
<label for="growthRate">Growth Rate:</label>
|
||||||
<input type="number" id="growthRate" value="1" min="0.1" max="10" step="0.1" />
|
<input type="number" id="growthRate" value="1" min="0.1" max="10" step="0.1" />
|
||||||
|
|
||||||
<label for="statesGrowthRate">States Growth Rate:</label>
|
<label for="statesGrowthRate">States Growth Rate:</label>
|
||||||
<input type="number" id="statesGrowthRate" value="1" min="0.1" max="10" step="0.1" />
|
<input type="number" id="statesGrowthRate" value="1" min="0.1" max="10" step="0.1" />
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Religions Settings -->
|
<!-- Religions Settings -->
|
||||||
|
|
@ -216,45 +226,47 @@
|
||||||
<input type="number" id="religionsNumber" value="5" min="0" max="99" />
|
<input type="number" id="religionsNumber" value="5" min="0" max="99" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Provinces Settings -->
|
<!-- Provinces Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Provinces</h3>
|
<h3>Provinces</h3>
|
||||||
<label for="provincesRatio">Provinces Ratio:</label>
|
<label for="provincesRatio">Provinces Ratio:</label>
|
||||||
<input type="number" id="provincesRatio" value="50" min="0" max="100" />
|
<input type="number" id="provincesRatio" value="50" min="0" max="100" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Military Settings -->
|
<!-- Military Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Military</h3>
|
<h3>Military</h3>
|
||||||
<label for="year">Year:</label>
|
<label for="year">Year:</label>
|
||||||
<input type="number" id="year" value="1400" />
|
<input type="number" id="year" value="1400" />
|
||||||
|
|
||||||
<label for="eraShort">Era (Short):</label>
|
<label for="eraShort">Era (Short):</label>
|
||||||
<input type="text" id="eraShort" value="AD" />
|
<input type="text" id="eraShort" value="AD" />
|
||||||
|
|
||||||
<label for="era">Era:</label>
|
<label for="era">Era:</label>
|
||||||
<input type="text" id="era" value="Anno Domini" />
|
<input type="text" id="era" value="Anno Domini" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Zones Settings -->
|
<!-- Zones Settings removed by Barrulus
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Zones</h3>
|
<h3>Zones</h3>
|
||||||
<label for="zonesGlobalModifier">Global Modifier:</label>
|
<label for="zonesGlobalModifier">Global Modifier:</label>
|
||||||
<input type="number" id="zonesGlobalModifier" value="1" min="0.1" max="10" step="0.1" />
|
<input type="number" id="zonesGlobalModifier" value="1" min="0.1" max="10" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Control Buttons -->
|
<!-- Control Buttons -->
|
||||||
<div class="config-section controls">
|
<div class="config-section controls">
|
||||||
<h3>Actions</h3>
|
<h3>Actions</h3>
|
||||||
<button id="newMapButton" class="primary">🗺️ Generate Map</button>
|
<button id="newMapButton" class="primary">🗺️ Generate Map</button>
|
||||||
<button id="generateButton" class="primary">Generate (Alt)</button>
|
|
||||||
|
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button id="saveConfigButton">💾 Save Config</button>
|
<button id="saveConfigButton">💾 Save Config</button>
|
||||||
<label for="loadConfigInput" class="button">📁 Load Config</label>
|
<label for="loadConfigInput" class="button">📁 Load Config</label>
|
||||||
<input type="file" id="loadConfigInput" accept=".json" style="display: none;" />
|
<input type="file" id="loadConfigInput" accept=".json" style="display: none;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="restoreSession" checked />
|
<input type="checkbox" id="restoreSession" checked />
|
||||||
Restore last session
|
Restore last session
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// main.js (Viewer Entry Point)
|
// main.js (Viewer Entry Point)
|
||||||
import './style.css';
|
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!");
|
console.log("FMG Viewer Initialized!");
|
||||||
|
|
||||||
// Example of how you will eventually use the engine
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
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', () => {
|
generateButton.addEventListener('click', () => {
|
||||||
console.log("Generating map...");
|
console.log("Generating map...");
|
||||||
|
|
||||||
|
|
@ -19,11 +19,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Call the engine
|
// 2. Call the engine
|
||||||
const mapData = generateMap(config);
|
const mapData = generate(config);
|
||||||
|
|
||||||
console.log("Map data generated by engine:", mapData);
|
console.log("Map data generated by engine:", mapData);
|
||||||
|
|
||||||
// 3. Render the map (this function will be built out later)
|
// 3. Render the map (this function will be built out later)
|
||||||
// renderMap(mapData);
|
// renderMap(mapData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import * as Routes from "./modules/routes-generator.js";
|
||||||
import * as Zones from "./modules/zones-generator.js";
|
import * as Zones from "./modules/zones-generator.js";
|
||||||
import * as voronoi from "./modules/voronoi.js";
|
import * as voronoi from "./modules/voronoi.js";
|
||||||
import * as Utils from "./utils/index.js";
|
import * as Utils from "./utils/index.js";
|
||||||
|
import * as graphUtils from "./utils/graphUtils.js"
|
||||||
|
|
||||||
// Import the new utility modules
|
// Import the new utility modules
|
||||||
import * as Graph from "./utils/graph.js";
|
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.
|
* @param {object} config - A comprehensive configuration object.
|
||||||
* @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }.
|
* @returns {object} An object containing the complete generated map data { grid, pack, notes, etc. }.
|
||||||
*/
|
*/
|
||||||
export function generate(config) {
|
export async function generate(config) {
|
||||||
const timeStart = performance.now();
|
const timeStart = performance.now();
|
||||||
|
|
||||||
// CORRECT: Get debug flags (values) from the config object.
|
// 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);
|
INFO && console.group("Generating Map with Seed: " + seed);
|
||||||
|
|
||||||
|
|
||||||
// 2. Pass the 'graph' section of the config to the new graph utilities
|
// 2. Pass the entire config to generateGrid (it needs graph and debug sections)
|
||||||
let grid = Graph.generateGrid(config.graph);
|
let grid = graphUtils.generateGrid(config);
|
||||||
|
|
||||||
// --- Heightmap and Features (assumed to be already modular) ---
|
// --- 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);
|
grid = Features.markupGrid(grid, config, Utils);
|
||||||
|
|
||||||
// 3. Pass 'map' and 'lakes' configs to the new geography utilities
|
// 3. Pass 'map' and 'lakes' configs to the new geography utilities
|
||||||
|
|
@ -64,30 +65,30 @@ export function generate(config) {
|
||||||
|
|
||||||
// --- Pack Generation ---
|
// --- Pack Generation ---
|
||||||
let pack = Graph.reGraph(grid, Utils);
|
let pack = Graph.reGraph(grid, Utils);
|
||||||
pack = Features.markupPack(pack, config, Utils, { Lakes });
|
pack = Features.markupPack(pack, grid, config, Utils, { Lakes });
|
||||||
|
|
||||||
// --- River Generation ---
|
// --- 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;
|
pack = riverResult.pack;
|
||||||
|
|
||||||
// --- Biome and Population ---
|
// --- Biome and Population ---
|
||||||
const { biome } = Biomes.define(pack, grid, config.biomes, Utils);
|
const { biome } = Biomes.define(pack, grid, config, Utils);
|
||||||
pack.cells.biome = biome;
|
pack.cells.biome = biome;
|
||||||
|
|
||||||
// 5. Call the new cell ranking utility
|
// 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.s = s;
|
||||||
pack.cells.pop = pop;
|
pack.cells.pop = pop;
|
||||||
|
|
||||||
// 6. Cultures, States, and Burgs
|
// 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 };
|
let packWithCultures = { ...pack, cultures: culturesResult.cultures };
|
||||||
packWithCultures.cells.culture = culturesResult.culture;
|
packWithCultures.cells.culture = culturesResult.culture;
|
||||||
|
|
||||||
const expandedCulturesData = Cultures.expand(packWithCultures, config.cultures, Utils, { biomesData: Biomes.getDefault() });
|
const expandedCulturesData = Cultures.expand(packWithCultures, config.cultures, Utils, { biomesData: Biomes.getDefault() });
|
||||||
pack = { ...packWithCultures, ...expandedCulturesData }; // Assumes expand returns an object with updated pack properties
|
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 = {
|
||||||
...pack,
|
...pack,
|
||||||
burgs: burgsAndStatesResult.burgs,
|
burgs: burgsAndStatesResult.burgs,
|
||||||
|
|
@ -96,34 +97,37 @@ export function generate(config) {
|
||||||
pack.cells.burg = burgsAndStatesResult.burg;
|
pack.cells.burg = burgsAndStatesResult.burg;
|
||||||
pack.cells.state = burgsAndStatesResult.state;
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
pack = { ...pack, ...zonesResult }; // Merge new zones data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,34 @@ export const getDefault = () => {
|
||||||
return {i: Array.from({length: name.length}, (_, i) => i), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
|
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) {
|
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 { d3, rn} = utils;
|
||||||
const { TIME } = config.debug;
|
const { TIME } = config.debug;
|
||||||
TIME && console.time("defineBiomes");
|
TIME && console.time("defineBiomes");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,34 @@
|
||||||
"use strict";
|
"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) => {
|
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 {cells, cultures} = pack;
|
||||||
const n = cells.i.length;
|
const n = cells.i.length;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,37 @@
|
||||||
"use strict";
|
"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 { WARN, ERROR, rand, rn, P, minmax, biased, rw, abbreviate } = utils;
|
||||||
const { TIME } = config.debug;
|
const { TIME } = config.debug;
|
||||||
|
const { Names } = modules;
|
||||||
|
|
||||||
TIME && console.time("generateCultures");
|
TIME && console.time("generateCultures");
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
|
|
||||||
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
|
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
|
||||||
|
|
||||||
const culturesInputNumber = config.culturesInput;
|
const culturesInputNumber = config.cultures.culturesInput;
|
||||||
const culturesInSetNumber = config.culturesInSetNumber;
|
const culturesInSetNumber = config.cultures.culturesInSetNumber;
|
||||||
let count = Math.min(culturesInputNumber, culturesInSetNumber);
|
let count = Math.min(culturesInputNumber, culturesInSetNumber);
|
||||||
|
|
||||||
const populated = cells.i.filter(i => cells.s[i]); // populated cells
|
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 centers = utils.d3.quadtree();
|
||||||
const colors = getColors(count, utils);
|
const colors = getColors(count, utils);
|
||||||
const emblemShape = config.emblemShape;
|
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"});
|
cultures.unshift({name: "Wildlands", i: 0, base: 1, origins: [null], shield: "round"});
|
||||||
|
|
||||||
// make sure all bases exist in nameBases
|
// 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");
|
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));
|
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;
|
return cellId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCultures(culturesNumber, config, pack, utils) {
|
function selectCultures(culturesNumber, config, pack, grid, utils) {
|
||||||
let defaultCultures = getDefault(culturesNumber, config, pack, utils);
|
let defaultCultures = getDefault(culturesNumber, config, pack, grid, utils);
|
||||||
const cultures = [];
|
const cultures = [];
|
||||||
|
|
||||||
pack.cultures?.forEach(function (culture) {
|
pack.cultures?.forEach(function (culture) {
|
||||||
|
|
@ -220,14 +241,14 @@ export const add = function (center, pack, config, utils) {
|
||||||
return newCulture;
|
return newCulture;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefault = function (count, config, pack, utils) {
|
export const getDefault = function (count, config, pack, grid, utils) {
|
||||||
// generic sorting functions
|
// generic sorting functions
|
||||||
const cells = pack.cells,
|
const cells = pack.cells,
|
||||||
s = cells.s,
|
s = cells.s,
|
||||||
sMax = utils.d3.max(s),
|
sMax = utils.d3.max(s),
|
||||||
t = cells.t,
|
t = cells.t,
|
||||||
h = cells.h,
|
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 n = cell => Math.ceil((s[cell] / sMax) * 3); // normalized cell score
|
||||||
const td = (cell, goal) => {
|
const td = (cell, goal) => {
|
||||||
const d = Math.abs(temp[cells.g[cell]] - goal);
|
const d = Math.abs(temp[cells.g[cell]] - goal);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const WATER_COAST = -1;
|
||||||
const DEEP_WATER = -2;
|
const DEEP_WATER = -2;
|
||||||
|
|
||||||
// calculate distance to coast for every cell
|
// 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) {
|
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
|
||||||
marked = 0;
|
marked = 0;
|
||||||
const prevDistance = distance - increment;
|
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) {
|
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 {rn} = utils;
|
||||||
const { TIME } = config.debug;
|
const { TIME } = config.debug;
|
||||||
|
|
||||||
|
|
@ -86,8 +109,31 @@ export function markupGrid(grid, config, utils) {
|
||||||
return updatedGrid;
|
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) {
|
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 {TIME} = config;
|
||||||
const {isLand, isWater, dist2, rn, clipPoly, unique, createTypedArray, connectVertices} = utils;
|
const {isLand, isWater, dist2, rn, clipPoly, unique, createTypedArray, connectVertices} = utils;
|
||||||
const {Lakes} = modules;
|
const {Lakes} = modules;
|
||||||
|
|
@ -111,7 +157,7 @@ export function markupPack(pack, grid, config, utils, modules) {
|
||||||
const firstCell = queue[0];
|
const firstCell = queue[0];
|
||||||
featureIds[firstCell] = featureId;
|
featureIds[firstCell] = featureId;
|
||||||
|
|
||||||
const land = isLand(firstCell);
|
const land = isLand(firstCell, pack);
|
||||||
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
|
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
|
||||||
let totalCells = 1; // count cells in a feature
|
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;
|
if (!border && borderCells[cellId]) border = true;
|
||||||
|
|
||||||
for (const neighborId of neighbors[cellId]) {
|
for (const neighborId of neighbors[cellId]) {
|
||||||
const isNeibLand = isLand(neighborId);
|
const isNeibLand = isLand(neighborId, pack);
|
||||||
|
|
||||||
if (land && !isNeibLand) {
|
if (land && !isNeibLand) {
|
||||||
distanceField[cellId] = LAND_COAST;
|
distanceField[cellId] = LAND_COAST;
|
||||||
|
|
@ -166,7 +212,7 @@ export function markupPack(pack, grid, config, utils, modules) {
|
||||||
return updatedPack;
|
return updatedPack;
|
||||||
|
|
||||||
function defineHaven(cellId) {
|
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 distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
|
||||||
const closest = distances.indexOf(Math.min.apply(Math, distances));
|
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}) {
|
function addFeature({firstCell, land, border, featureId, totalCells}) {
|
||||||
const type = land ? "island" : border ? "ocean" : "lake";
|
const type = land ? "island" : border ? "ocean" : "lake";
|
||||||
const [startCell, featureVertices] = getCellsData(type, firstCell);
|
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 area = d3.polygonArea(points); // feature perimiter area
|
||||||
const absArea = Math.abs(rn(area));
|
const absArea = Math.abs(rn(area));
|
||||||
|
|
||||||
|
|
@ -194,8 +240,8 @@ export function markupPack(pack, grid, config, utils, modules) {
|
||||||
|
|
||||||
if (type === "lake") {
|
if (type === "lake") {
|
||||||
if (area > 0) feature.vertices = feature.vertices.reverse();
|
if (area > 0) feature.vertices = feature.vertices.reverse();
|
||||||
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
|
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(i => isLand(i, pack))).flat());
|
||||||
feature.height = Lakes.getHeight(feature);
|
feature.height = Lakes.getHeight(feature, pack, utils);
|
||||||
}
|
}
|
||||||
|
|
||||||
return feature;
|
return feature;
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,73 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export async function generate(graph, config, utils) {
|
export function fromTemplate(grid, templateId, config, utils) {
|
||||||
const { aleaPRNG, heightmapTemplates } = utils;
|
const { heightmapTemplates, aleaPRNG } = utils;
|
||||||
const { TIME } = config.debug;
|
const templateString = heightmapTemplates[templateId]?.template || "";
|
||||||
const { templateId, seed } = config;
|
|
||||||
|
|
||||||
TIME && console.time("defineHeightmap");
|
|
||||||
|
|
||||||
const isTemplate = templateId in heightmapTemplates;
|
|
||||||
const heights = isTemplate
|
|
||||||
? fromTemplate(graph, templateId, config, utils)
|
|
||||||
: await fromPrecreated(graph, templateId, config, utils);
|
|
||||||
|
|
||||||
TIME && console.timeEnd("defineHeightmap");
|
|
||||||
|
|
||||||
return heights;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder function for processing precreated heightmaps
|
|
||||||
// This will need further refactoring to work headlessly (see heightmap-generator_render.md)
|
|
||||||
export async function fromPrecreated(graph, id, config, utils) {
|
|
||||||
// TODO: Implement headless image processing
|
|
||||||
// This function currently requires DOM/Canvas which was removed
|
|
||||||
// Future implementation will need:
|
|
||||||
// - utils.loadImage() function to load PNG files headlessly
|
|
||||||
// - Image processing library (e.g., canvas package for Node.js)
|
|
||||||
// - getHeightsFromImageData() refactored for headless operation
|
|
||||||
throw new Error(`fromPrecreated not yet implemented for headless operation. Template ID: ${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromTemplate(graph, id, config, utils) {
|
|
||||||
const { heightmapTemplates } = utils;
|
|
||||||
const templateString = heightmapTemplates[id]?.template || "";
|
|
||||||
const steps = templateString.split("\n");
|
const steps = templateString.split("\n");
|
||||||
|
|
||||||
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
|
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${templateId}. Steps: ${steps}`);
|
||||||
|
|
||||||
let { heights, blobPower, linePower } = setGraph(graph, utils);
|
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) {
|
for (const step of steps) {
|
||||||
const elements = step.trim().split(" ");
|
const elements = step.trim().split(" ");
|
||||||
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
|
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${templateId}. Step: ${elements}`);
|
||||||
heights = addStep(heights, graph, blobPower, linePower, config, utils, ...elements);
|
heights = addStep(heights, grid, blobPower, linePower, utils, ...elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
return heights;
|
return heights;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGraph(graph, utils) {
|
export async function fromPrecreated(grid, imageId, config, utils) {
|
||||||
const { createTypedArray } = utils;
|
// This function requires browser-specific Canvas API and Image loading
|
||||||
const { cellsDesired, cells, points } = graph;
|
// It should be handled by the viewer layer, not the headless engine
|
||||||
const heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
|
throw new Error("fromPrecreated requires browser environment - should be handled by viewer layer");
|
||||||
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) {
|
export async function generate(grid, config, utils) {
|
||||||
if (tool === "Hill") return addHill(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
|
const { TIME, aleaPRNG } = utils;
|
||||||
if (tool === "Pit") return addPit(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
|
TIME && console.time("defineHeightmap");
|
||||||
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);
|
const templateId = config.heightmap.templateId;
|
||||||
if (tool === "Strait") return addStrait(heights, graph, config, utils, a2, a3);
|
|
||||||
if (tool === "Mask") return mask(heights, graph, config, utils, a2);
|
// Set up PRNG if seed is provided
|
||||||
if (tool === "Invert") return invert(heights, graph, config, utils, a2, a3);
|
if (config.seed !== undefined) {
|
||||||
if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
|
Math.random = aleaPRNG(config.seed);
|
||||||
if (tool === "Multiply") return modify(heights, a3, 0, +a2, utils);
|
}
|
||||||
if (tool === "Smooth") return smooth(heights, graph, utils, a2);
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
return heights;
|
return heights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,12 +110,13 @@ function getLinePower(cells) {
|
||||||
return linePowerMap[cells] || 0.81;
|
return linePowerMap[cells] || 0.81;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addHill(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
|
function addHill(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
|
||||||
const { getNumberInRange, lim, findGridCell } = utils;
|
const { getNumberInRange, findGridCell, lim } = utils;
|
||||||
const { graphWidth, graphHeight } = config;
|
const graphWidth = grid.cellsX;
|
||||||
|
const graphHeight = grid.cellsY;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
|
let newHeights = new Uint8Array(heights);
|
||||||
|
|
||||||
while (count > 0) {
|
while (count > 0) {
|
||||||
addOneHill();
|
addOneHill();
|
||||||
|
|
@ -131,34 +132,35 @@ export function addHill(heights, graph, blobPower, config, utils, count, height,
|
||||||
do {
|
do {
|
||||||
const x = getPointInRange(rangeX, graphWidth, utils);
|
const x = getPointInRange(rangeX, graphWidth, utils);
|
||||||
const y = getPointInRange(rangeY, graphHeight, utils);
|
const y = getPointInRange(rangeY, graphHeight, utils);
|
||||||
start = findGridCell(x, y, graph);
|
start = findGridCell(x, y, grid);
|
||||||
limit++;
|
limit++;
|
||||||
} while (heights[start] + h > 90 && limit < 50);
|
} while (newHeights[start] + h > 90 && limit < 50);
|
||||||
|
|
||||||
change[start] = h;
|
change[start] = h;
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const q = queue.shift();
|
const q = queue.shift();
|
||||||
|
|
||||||
for (const c of graph.cells.c[q]) {
|
for (const c of grid.cells.c[q]) {
|
||||||
if (change[c]) continue;
|
if (change[c]) continue;
|
||||||
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
|
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||||
if (change[c] > 1) queue.push(c);
|
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) {
|
function addPit(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
|
||||||
const { getNumberInRange, lim, findGridCell } = utils;
|
const { getNumberInRange, findGridCell, lim } = utils;
|
||||||
const { graphWidth, graphHeight } = config;
|
const graphWidth = grid.cellsX;
|
||||||
|
const graphHeight = grid.cellsY;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
|
let newHeights = new Uint8Array(heights);
|
||||||
|
|
||||||
while (count > 0) {
|
while (count > 0) {
|
||||||
addOnePit();
|
addOnePit();
|
||||||
|
|
@ -167,16 +169,15 @@ export function addPit(heights, graph, blobPower, config, utils, count, height,
|
||||||
|
|
||||||
function addOnePit() {
|
function addOnePit() {
|
||||||
const used = new Uint8Array(heights.length);
|
const used = new Uint8Array(heights.length);
|
||||||
let limit = 0,
|
let limit = 0, start;
|
||||||
start;
|
|
||||||
let h = lim(getNumberInRange(height));
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const x = getPointInRange(rangeX, graphWidth, utils);
|
const x = getPointInRange(rangeX, graphWidth, utils);
|
||||||
const y = getPointInRange(rangeY, graphHeight, utils);
|
const y = getPointInRange(rangeY, graphHeight, utils);
|
||||||
start = findGridCell(x, y, graph);
|
start = findGridCell(x, y, grid);
|
||||||
limit++;
|
limit++;
|
||||||
} while (heights[start] < 20 && limit < 50);
|
} while (newHeights[start] < 20 && limit < 50);
|
||||||
|
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
while (queue.length) {
|
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);
|
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||||
if (h < 1) return;
|
if (h < 1) return;
|
||||||
|
|
||||||
graph.cells.c[q].forEach(function (c, i) {
|
grid.cells.c[q].forEach(function (c, i) {
|
||||||
if (used[c]) return;
|
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;
|
used[c] = 1;
|
||||||
queue.push(c);
|
queue.push(c);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return heights;
|
return newHeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addRange(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
|
// fromCell, toCell are options cell ids
|
||||||
const { getNumberInRange, lim, findGridCell, d3 } = utils;
|
function addRange(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
|
||||||
const { graphWidth, graphHeight } = config;
|
const { getNumberInRange, findGridCell, lim, d3 } = utils;
|
||||||
|
const graphWidth = grid.cellsX;
|
||||||
|
const graphHeight = grid.cellsY;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
|
let newHeights = new Uint8Array(heights);
|
||||||
|
|
||||||
while (count > 0) {
|
while (count > 0) {
|
||||||
addOneRange();
|
addOneRange();
|
||||||
|
|
@ -217,10 +220,7 @@ export function addRange(heights, graph, linePower, config, utils, count, height
|
||||||
const startX = getPointInRange(rangeX, graphWidth, utils);
|
const startX = getPointInRange(rangeX, graphWidth, utils);
|
||||||
const startY = getPointInRange(rangeY, graphHeight, utils);
|
const startY = getPointInRange(rangeY, graphHeight, utils);
|
||||||
|
|
||||||
let dist = 0,
|
let dist = 0, limit = 0, endX, endY;
|
||||||
limit = 0,
|
|
||||||
endX,
|
|
||||||
endY;
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
||||||
|
|
@ -229,8 +229,8 @@ export function addRange(heights, graph, linePower, config, utils, count, height
|
||||||
limit++;
|
limit++;
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
|
||||||
|
|
||||||
startCell = findGridCell(startX, startY, graph);
|
startCell = findGridCell(startX, startY, grid);
|
||||||
endCell = findGridCell(endX, endY, graph);
|
endCell = findGridCell(endX, endY, grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = getRange(startCell, endCell);
|
let range = getRange(startCell, endCell);
|
||||||
|
|
@ -238,12 +238,12 @@ export function addRange(heights, graph, linePower, config, utils, count, height
|
||||||
// get main ridge
|
// get main ridge
|
||||||
function getRange(cur, end) {
|
function getRange(cur, end) {
|
||||||
const range = [cur];
|
const range = [cur];
|
||||||
const p = graph.points;
|
const p = grid.points;
|
||||||
used[cur] = 1;
|
used[cur] = 1;
|
||||||
|
|
||||||
while (cur !== end) {
|
while (cur !== end) {
|
||||||
let min = Infinity;
|
let min = Infinity;
|
||||||
graph.cells.c[cur].forEach(function (e) {
|
grid.cells.c[cur].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
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 (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
|
// add height to ridge and cells around
|
||||||
let queue = range.slice(),
|
let queue = range.slice(), i = 0;
|
||||||
i = 0;
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const frontier = queue.slice();
|
const frontier = queue.slice();
|
||||||
(queue = []), i++;
|
(queue = []), i++;
|
||||||
frontier.forEach(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;
|
h = h ** linePower - 1;
|
||||||
if (h < 2) break;
|
if (h < 2) break;
|
||||||
frontier.forEach(f => {
|
frontier.forEach(f => {
|
||||||
graph.cells.c[f].forEach(i => {
|
grid.cells.c[f].forEach(i => {
|
||||||
if (!used[i]) {
|
if (!used[i]) {
|
||||||
queue.push(i);
|
queue.push(i);
|
||||||
used[i] = 1;
|
used[i] = 1;
|
||||||
|
|
@ -285,22 +284,23 @@ export function addRange(heights, graph, linePower, config, utils, count, height
|
||||||
range.forEach((cur, d) => {
|
range.forEach((cur, d) => {
|
||||||
if (d % 6 !== 0) return;
|
if (d % 6 !== 0) return;
|
||||||
for (const l of d3.range(i)) {
|
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
|
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
|
||||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
|
||||||
cur = min;
|
cur = min;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return heights;
|
return newHeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTrough(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
|
function addTrough(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
|
||||||
const { getNumberInRange, lim, findGridCell, d3 } = utils;
|
const { getNumberInRange, findGridCell, lim, d3 } = utils;
|
||||||
const { graphWidth, graphHeight } = config;
|
const graphWidth = grid.cellsX;
|
||||||
|
const graphHeight = grid.cellsY;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
|
let newHeights = new Uint8Array(heights);
|
||||||
|
|
||||||
while (count > 0) {
|
while (count > 0) {
|
||||||
addOneTrough();
|
addOneTrough();
|
||||||
|
|
@ -313,18 +313,13 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
|
||||||
|
|
||||||
if (rangeX && rangeY) {
|
if (rangeX && rangeY) {
|
||||||
// find start and end points
|
// find start and end points
|
||||||
let limit = 0,
|
let limit = 0, startX, startY, dist = 0, endX, endY;
|
||||||
startX,
|
|
||||||
startY,
|
|
||||||
dist = 0,
|
|
||||||
endX,
|
|
||||||
endY;
|
|
||||||
do {
|
do {
|
||||||
startX = getPointInRange(rangeX, graphWidth, utils);
|
startX = getPointInRange(rangeX, graphWidth, utils);
|
||||||
startY = getPointInRange(rangeY, graphHeight, utils);
|
startY = getPointInRange(rangeY, graphHeight, utils);
|
||||||
startCell = findGridCell(startX, startY, graph);
|
startCell = findGridCell(startX, startY, grid);
|
||||||
limit++;
|
limit++;
|
||||||
} while (heights[startCell] < 20 && limit < 50);
|
} while (newHeights[startCell] < 20 && limit < 50);
|
||||||
|
|
||||||
limit = 0;
|
limit = 0;
|
||||||
do {
|
do {
|
||||||
|
|
@ -334,7 +329,7 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
|
||||||
limit++;
|
limit++;
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
|
||||||
|
|
||||||
endCell = findGridCell(endX, endY, graph);
|
endCell = findGridCell(endX, endY, grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = getRange(startCell, endCell);
|
let range = getRange(startCell, endCell);
|
||||||
|
|
@ -342,12 +337,12 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
|
||||||
// get main ridge
|
// get main ridge
|
||||||
function getRange(cur, end) {
|
function getRange(cur, end) {
|
||||||
const range = [cur];
|
const range = [cur];
|
||||||
const p = graph.points;
|
const p = grid.points;
|
||||||
used[cur] = 1;
|
used[cur] = 1;
|
||||||
|
|
||||||
while (cur !== end) {
|
while (cur !== end) {
|
||||||
let min = Infinity;
|
let min = Infinity;
|
||||||
graph.cells.c[cur].forEach(function (e) {
|
grid.cells.c[cur].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
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 (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
|
// add height to ridge and cells around
|
||||||
let queue = range.slice(),
|
let queue = range.slice(), i = 0;
|
||||||
i = 0;
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const frontier = queue.slice();
|
const frontier = queue.slice();
|
||||||
(queue = []), i++;
|
(queue = []), i++;
|
||||||
frontier.forEach(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;
|
h = h ** linePower - 1;
|
||||||
if (h < 2) break;
|
if (h < 2) break;
|
||||||
frontier.forEach(f => {
|
frontier.forEach(f => {
|
||||||
graph.cells.c[f].forEach(i => {
|
grid.cells.c[f].forEach(i => {
|
||||||
if (!used[i]) {
|
if (!used[i]) {
|
||||||
queue.push(i);
|
queue.push(i);
|
||||||
used[i] = 1;
|
used[i] = 1;
|
||||||
|
|
@ -389,25 +383,25 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
|
||||||
range.forEach((cur, d) => {
|
range.forEach((cur, d) => {
|
||||||
if (d % 6 !== 0) return;
|
if (d % 6 !== 0) return;
|
||||||
for (const l of d3.range(i)) {
|
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
|
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
|
||||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
|
||||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
|
||||||
cur = min;
|
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 { 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), grid.cellsX / 3);
|
||||||
width = Math.min(getNumberInRange(width), graph.cellsX / 3);
|
|
||||||
if (width < 1 && P(width)) return heights;
|
if (width < 1 && P(width)) return heights;
|
||||||
|
|
||||||
|
let newHeights = new Uint8Array(heights);
|
||||||
const used = new Uint8Array(heights.length);
|
const used = new Uint8Array(heights.length);
|
||||||
const vert = direction === "vertical";
|
const vert = direction === "vertical";
|
||||||
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
|
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
|
? graphHeight - 5
|
||||||
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
|
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
|
||||||
|
|
||||||
const start = findGridCell(startX, startY, graph);
|
const start = findGridCell(startX, startY, grid);
|
||||||
const end = findGridCell(endX, endY, graph);
|
const end = findGridCell(endX, endY, grid);
|
||||||
let range = getRange(start, end);
|
let range = getRange(start, end);
|
||||||
const query = [];
|
const query = [];
|
||||||
|
|
||||||
function getRange(cur, end) {
|
function getRange(cur, end) {
|
||||||
const range = [];
|
const range = [];
|
||||||
const p = graph.points;
|
const p = grid.points;
|
||||||
|
|
||||||
while (cur !== end) {
|
while (cur !== end) {
|
||||||
let min = Infinity;
|
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;
|
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 (Math.random() > 0.8) diff = diff / 2;
|
||||||
if (diff < min) {
|
if (diff < min) {
|
||||||
|
|
@ -449,12 +443,12 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
|
||||||
while (width > 0) {
|
while (width > 0) {
|
||||||
const exp = 0.9 - step * width;
|
const exp = 0.9 - step * width;
|
||||||
range.forEach(function (r) {
|
range.forEach(function (r) {
|
||||||
graph.cells.c[r].forEach(function (e) {
|
grid.cells.c[r].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
used[e] = 1;
|
used[e] = 1;
|
||||||
query.push(e);
|
query.push(e);
|
||||||
heights[e] **= exp;
|
newHeights[e] **= exp;
|
||||||
if (heights[e] > 100) heights[e] = 5;
|
if (newHeights[e] > 100) newHeights[e] = 5;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
range = query.slice();
|
range = query.slice();
|
||||||
|
|
@ -462,18 +456,16 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
|
||||||
width--;
|
width--;
|
||||||
}
|
}
|
||||||
|
|
||||||
return heights;
|
return newHeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modify(heights, range, add, mult, power, utils) {
|
function modify(heights, range, add, mult, power) {
|
||||||
const { lim } = utils;
|
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||||
const isLand = min === 20;
|
const isLand = min === 20;
|
||||||
|
|
||||||
heights = heights.map(h => {
|
return heights.map(h => {
|
||||||
if (h < min || h > max) return h;
|
if (h < min || h > max) return h;
|
||||||
|
|
||||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
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;
|
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
|
||||||
return lim(h);
|
return lim(h);
|
||||||
});
|
});
|
||||||
|
|
||||||
return heights;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function smooth(heights, graph, utils, fr = 2, add = 0) {
|
function smooth(heights, grid, utils, fr = 2, add = 0) {
|
||||||
const { lim, d3 } = utils;
|
const { d3, lim } = utils;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
return heights.map((h, i) => {
|
||||||
heights = heights.map((h, i) => {
|
|
||||||
const a = [h];
|
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;
|
if (fr === 1) return d3.mean(a) + add;
|
||||||
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return heights;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mask(heights, graph, config, utils, power = 1) {
|
function mask(heights, grid, power = 1) {
|
||||||
const { lim } = utils;
|
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
|
||||||
const { graphWidth, graphHeight } = config;
|
const graphWidth = grid.cellsX;
|
||||||
|
const graphHeight = grid.cellsY;
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
const fr = power ? Math.abs(power) : 1;
|
const fr = power ? Math.abs(power) : 1;
|
||||||
|
|
||||||
heights = heights.map((h, i) => {
|
return heights.map((h, i) => {
|
||||||
const [x, y] = graph.points[i];
|
const [x, y] = grid.points[i];
|
||||||
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
|
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
|
||||||
const ny = (2 * y) / graphHeight - 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
|
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;
|
const masked = h * distance;
|
||||||
return lim((h * (fr - 1) + masked) / fr);
|
return lim((h * (fr - 1) + masked) / fr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return heights;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invert(heights, graph, config, utils, count, axes) {
|
function invert(heights, grid, count, axes) {
|
||||||
const { P } = utils;
|
const { P } = { P: probability => Math.random() < probability };
|
||||||
|
|
||||||
if (!P(count)) return heights;
|
if (!P(count)) return heights;
|
||||||
|
|
||||||
heights = new Uint8Array(heights);
|
|
||||||
const invertX = axes !== "y";
|
const invertX = axes !== "y";
|
||||||
const invertY = axes !== "x";
|
const invertY = axes !== "x";
|
||||||
const { cellsX, cellsY } = graph;
|
const { cellsX, cellsY } = grid;
|
||||||
|
|
||||||
const inverted = heights.map((h, i) => {
|
const inverted = heights.map((h, i) => {
|
||||||
const x = i % cellsX;
|
const x = i % cellsX;
|
||||||
|
|
@ -543,10 +525,9 @@ export function invert(heights, graph, config, utils, count, axes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointInRange(range, length, utils) {
|
function getPointInRange(range, length, utils) {
|
||||||
const { rand } = utils;
|
const { ERROR, rand } = utils;
|
||||||
|
|
||||||
if (typeof range !== "string") {
|
if (typeof range !== "string") {
|
||||||
console.error("Range should be a string");
|
ERROR && console.error("Range should be a string");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,3 +535,20 @@ function getPointInRange(range, length, utils) {
|
||||||
const max = range.split("-")[1] / 100 || min;
|
const max = range.split("-")[1] / 100 || min;
|
||||||
return rand(min * length, max * length);
|
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
|
||||||
|
};
|
||||||
|
|
|
||||||
578
procedural/src/engine/modules/heightmap-generator_broken.js
Normal file
578
procedural/src/engine/modules/heightmap-generator_broken.js
Normal 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);
|
||||||
|
}
|
||||||
35
procedural/src/engine/modules/heightmap-generator_config.md
Normal file
35
procedural/src/engine/modules/heightmap-generator_config.md
Normal 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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
@ -2,8 +2,31 @@
|
||||||
|
|
||||||
const LAKE_ELEVATION_DELTA = 0.1;
|
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) {
|
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 {cells, features} = pack;
|
||||||
const ELEVATION_LIMIT = config.lakeElevationLimit;
|
const ELEVATION_LIMIT = config.lakeElevationLimit;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,32 @@
|
||||||
"use strict";
|
"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) {
|
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 { minmax, rn, ra, rand, gauss, si, nth, d3, populationRate, urbanization} = utils;
|
||||||
const { TIME } = config.debug;
|
const { TIME } = config.debug;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export function generateOceanLayers(grid, config, utils) {
|
||||||
if (relaxed.length < 4) continue;
|
if (relaxed.length < 4) continue;
|
||||||
const points = clipPoly(
|
const points = clipPoly(
|
||||||
relaxed.map(v => vertices.p[v]),
|
relaxed.map(v => vertices.p[v]),
|
||||||
|
config,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
chains.push([t, points]);
|
chains.push([t, points]);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,38 @@ const forms = {
|
||||||
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
|
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) => {
|
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 {
|
const {
|
||||||
generateSeed,
|
generateSeed,
|
||||||
aleaPRNG,
|
aleaPRNG,
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,38 @@ const expansionismMap = {
|
||||||
Heresy: (utils) => utils.gauss(1, 0.5, 0, 5, 1)
|
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) {
|
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;
|
const { TIME } = config.debug;
|
||||||
TIME && console.time("generateReligions");
|
TIME && console.time("generateReligions");
|
||||||
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
|
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,38 @@
|
||||||
"use strict";
|
"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) {
|
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 { seed, aleaPRNG, resolveDepressionsSteps, cellsCount, graphWidth, graphHeight, WARN} = config;
|
||||||
const {rn, rw, each, round, d3, lineGen} = utils;
|
const {rn, rw, each, round, d3, lineGen} = utils;
|
||||||
const {Lakes, Names} = modules;
|
const {Lakes, Names} = modules;
|
||||||
|
|
@ -27,13 +59,13 @@ export const generate = function (pack, grid, config, utils, modules, allowErosi
|
||||||
let riverNext = 1; // first river id is 1
|
let riverNext = 1; // first river id is 1
|
||||||
|
|
||||||
const h = alterHeights(pack, utils);
|
const h = alterHeights(pack, utils);
|
||||||
Lakes.detectCloseLakes(h);
|
Lakes.detectCloseLakes(pack, grid, h, config);
|
||||||
const resolvedH = resolveDepressions(pack, config, utils, h);
|
const resolvedH = resolveDepressions(pack, config, utils, h);
|
||||||
const {updatedCells, updatedFeatures, updatedRivers} = drainWater(pack, grid, config, utils, modules, newCells, resolvedH, riversData, riverParents, riverNext);
|
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);
|
const {finalCells, finalRivers} = defineRivers(pack, config, utils, updatedCells, riversData, riverParents);
|
||||||
|
|
||||||
calculateConfluenceFlux(finalCells, resolvedH);
|
calculateConfluenceFlux(finalCells, resolvedH);
|
||||||
Lakes.cleanupLakeData();
|
Lakes.cleanupLakeData(pack);
|
||||||
|
|
||||||
let finalH = resolvedH;
|
let finalH = resolvedH;
|
||||||
if (allowErosion) {
|
if (allowErosion) {
|
||||||
|
|
@ -64,7 +96,7 @@ export const generate = function (pack, grid, config, utils, modules, allowErosi
|
||||||
|
|
||||||
const prec = grid.cells.prec;
|
const prec = grid.cells.prec;
|
||||||
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
|
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) {
|
land.forEach(function (i) {
|
||||||
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,34 @@ const ROUTE_TYPE_MODIFIERS = {
|
||||||
default: 8 // far ocean
|
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 = []) {
|
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 { dist2, findPath, findCell, rn } = utils;
|
||||||
const { capitalsByFeature, burgsByFeature, portsByFeature } = sortBurgsByFeature(pack.burgs);
|
const { capitalsByFeature, burgsByFeature, portsByFeature } = sortBurgsByFeature(pack.burgs);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// FMG utils related to cell ranking and population
|
// FMG utils related to cell ranking and population
|
||||||
|
|
||||||
// calculate cell suitability and population based on various factors
|
// calculate cell suitability and population based on various factors
|
||||||
function rankCells(pack, grid, utils, modules) {
|
function rankCells(pack, grid, config, utils, modules) {
|
||||||
const { normalize } = utils;
|
const { normalize } = utils;
|
||||||
const { TIME } = config.debug;
|
const { TIME } = config.debug;
|
||||||
const { biomesData } = modules;
|
const { biomesData } = modules;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
import { polygonclip } from './lineclip.js';
|
||||||
|
|
||||||
// FMG helper functions
|
// FMG helper functions
|
||||||
|
|
||||||
// clip polygon by graph bbox
|
// clip polygon by graph bbox
|
||||||
function clipPoly(points, secure = 0) {
|
function clipPoly(points, config, secure = 0) {
|
||||||
if (points.length < 2) return points;
|
if (points.length < 2) return points;
|
||||||
if (points.some(point => point === undefined)) {
|
if (points.some(point => point === undefined)) {
|
||||||
ERROR && console.error("Undefined point in clipPoly", points);
|
console.error("Undefined point in clipPoly", points);
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const graphWidth = config.graph.width || 1000;
|
||||||
|
const graphHeight = config.graph.height || 1000;
|
||||||
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,6 @@ import { createTypedArray } from './arrayUtils.js';
|
||||||
// import { UINT16_MAX } from './arrayUtils.js';
|
// import { UINT16_MAX } from './arrayUtils.js';
|
||||||
import * as d3 from 'd3'; // Or import specific d3 modules
|
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
|
// place random points to calculate Voronoi diagram
|
||||||
function placePoints(config) {
|
function placePoints(config) {
|
||||||
|
|
@ -38,11 +27,20 @@ function calculateVoronoi(points, boundary) {
|
||||||
|
|
||||||
const cells = voronoi.cells;
|
const cells = voronoi.cells;
|
||||||
cells.i = createTypedArray({ maxValue: points.length, length: points.length }).map((_, i) => i);
|
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;
|
const vertices = voronoi.vertices;
|
||||||
|
|
||||||
return { cells, vertices };
|
return { cells, vertices };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// add points along map edge to pseudo-clip voronoi cells
|
// add points along map edge to pseudo-clip voronoi cells
|
||||||
function getBoundaryPoints(width, height, spacing) {
|
function getBoundaryPoints(width, height, spacing) {
|
||||||
const offset = rn(-1 * spacing);
|
const offset = rn(-1 * spacing);
|
||||||
|
|
|
||||||
332
procedural/src/engine/utils/graphUtils.js
Normal file
332
procedural/src/engine/utils/graphUtils.js
Normal 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 can’t contain a closer node.
|
||||||
|
if (
|
||||||
|
!(t.node = t.q.node) ||
|
||||||
|
(t.x1 = t.q.x0) > t.x3 ||
|
||||||
|
(t.y1 = t.q.y0) > t.y3 ||
|
||||||
|
(t.x2 = t.q.x1) < t.x0 ||
|
||||||
|
(t.y2 = t.q.y1) < t.y0
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Bisect the current quadrant.
|
||||||
|
if (t.node.length) {
|
||||||
|
t.node.explored = true;
|
||||||
|
var xm = (t.x1 + t.x2) / 2,
|
||||||
|
ym = (t.y1 + t.y2) / 2;
|
||||||
|
|
||||||
|
t.quads.push(
|
||||||
|
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
||||||
|
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
||||||
|
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
||||||
|
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visit the closest quadrant first.
|
||||||
|
if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
|
||||||
|
t.q = t.quads[t.quads.length - 1];
|
||||||
|
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
||||||
|
t.quads[t.quads.length - 1 - t.i] = t.q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit this point. (Visiting coincident points isn’t necessary!)
|
||||||
|
else {
|
||||||
|
var dx = x - +this._x.call(null, t.node.data),
|
||||||
|
dy = y - +this._y.call(null, t.node.data),
|
||||||
|
d2 = dx * dx + dy * dy;
|
||||||
|
radiusSearchVisit(t, d2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.result;
|
||||||
|
};
|
||||||
|
d3.quadtree.prototype.findAll = tree_filter;
|
||||||
|
|
||||||
|
var radiusSearchInit = function (t, radius) {
|
||||||
|
t.result = [];
|
||||||
|
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
|
||||||
|
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
|
||||||
|
t.radius = radius * radius;
|
||||||
|
};
|
||||||
|
|
||||||
|
var radiusSearchVisit = function (t, d2) {
|
||||||
|
t.node.data.scanned = true;
|
||||||
|
if (d2 < t.radius) {
|
||||||
|
do {
|
||||||
|
t.result.push(t.node.data);
|
||||||
|
t.node.data.selected = true;
|
||||||
|
} while ((t.node = t.node.next));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Export all functions
|
||||||
|
export {
|
||||||
|
generateGrid,
|
||||||
|
placePoints,
|
||||||
|
calculateVoronoi,
|
||||||
|
getBoundaryPoints,
|
||||||
|
getJitteredGrid,
|
||||||
|
findGridCell,
|
||||||
|
findGridAll,
|
||||||
|
find,
|
||||||
|
findCell,
|
||||||
|
findAll,
|
||||||
|
getPackPolygon,
|
||||||
|
getGridPolygon,
|
||||||
|
poissonDiscSampler,
|
||||||
|
isLand,
|
||||||
|
isWater
|
||||||
|
};
|
||||||
168
procedural/src/engine/utils/heightmap-templates.js
Normal file
168
procedural/src/engine/utils/heightmap-templates.js
Normal 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 };
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import "./polyfills.js";
|
import "./polyfills.js";
|
||||||
|
import * as d3 from 'd3';
|
||||||
|
|
||||||
export { aleaPRNG } from './alea.js'
|
export { aleaPRNG } from './alea.js'
|
||||||
|
export { d3 };
|
||||||
export {
|
export {
|
||||||
last,
|
last,
|
||||||
unique,
|
unique,
|
||||||
|
|
@ -39,9 +41,25 @@ export {
|
||||||
} from "./debugUtils.js";
|
} from "./debugUtils.js";
|
||||||
export { rollups, nest, dist2 } from "./functionUtils.js";
|
export { rollups, nest, dist2 } from "./functionUtils.js";
|
||||||
export {
|
export {
|
||||||
generateGrid,
|
reGraph
|
||||||
reGraph,
|
|
||||||
} from "./graph.js";
|
} from "./graph.js";
|
||||||
|
export {
|
||||||
|
generateGrid,
|
||||||
|
placePoints,
|
||||||
|
calculateVoronoi,
|
||||||
|
getBoundaryPoints,
|
||||||
|
getJitteredGrid,
|
||||||
|
findGridCell,
|
||||||
|
findGridAll,
|
||||||
|
find,
|
||||||
|
findCell,
|
||||||
|
findAll,
|
||||||
|
getPackPolygon,
|
||||||
|
getGridPolygon,
|
||||||
|
poissonDiscSampler,
|
||||||
|
isLand,
|
||||||
|
isWater
|
||||||
|
} from "./graphUtils.js";
|
||||||
export {
|
export {
|
||||||
removeParent,
|
removeParent,
|
||||||
getComposedPath,
|
getComposedPath,
|
||||||
|
|
@ -88,3 +106,4 @@ export {
|
||||||
export { convertTemperature, si, getInteger } from "./unitUtils.js";
|
export { convertTemperature, si, getInteger } from "./unitUtils.js";
|
||||||
export { simplify } from "./simplify.js";
|
export { simplify } from "./simplify.js";
|
||||||
export { lineclip } from "./lineclip.js";
|
export { lineclip } from "./lineclip.js";
|
||||||
|
export { heightmapTemplates } from "./heightmap-templates.js";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
// lineclip by mourner, https://github.com/mapbox/lineclip
|
// 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}
|
"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 };
|
||||||
|
|
@ -78,6 +78,9 @@ export function validateConfig(config) {
|
||||||
// Validate logical constraints
|
// Validate logical constraints
|
||||||
validateLogicalConstraints(config, result);
|
validateLogicalConstraints(config, result);
|
||||||
|
|
||||||
|
// Validate required fields for modules
|
||||||
|
validateRequiredFields(config, result);
|
||||||
|
|
||||||
// Set valid flag based on errors
|
// Set valid flag based on errors
|
||||||
result.valid = result.errors.length === 0;
|
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
|
* Helper function to get nested property value
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import {
|
||||||
/**
|
/**
|
||||||
* Handle map generation with validation
|
* Handle map generation with validation
|
||||||
*/
|
*/
|
||||||
function handleGenerateClick() {
|
async function handleGenerateClick() {
|
||||||
console.log("Building config from UI and calling engine...");
|
console.log("Building config from UI and calling engine...");
|
||||||
|
|
||||||
// Build configuration from current UI state
|
// Build configuration from current UI state
|
||||||
|
|
@ -67,7 +67,7 @@ function handleGenerateClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single, clean call to the engine with validated config
|
// 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);
|
console.log("Engine finished. Map data generated:", mapData);
|
||||||
// The renderer will take over from here
|
// The renderer will take over from here
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue