1026 lines
32 KiB
Markdown
1026 lines
32 KiB
Markdown
|
|
# McRogueFace Procedural Generation System Specification
|
|||
|
|
|
|||
|
|
**Version:** 1.0 Draft
|
|||
|
|
**Date:** 2026-01-11
|
|||
|
|
**Status:** Design Complete, Pending Implementation
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
This specification defines the procedural generation system for McRogueFace, exposing libtcod's noise, BSP, and heightmap capabilities through a Pythonic interface optimized for batch operations on Grid and Layer objects.
|
|||
|
|
|
|||
|
|
### Design Philosophy
|
|||
|
|
|
|||
|
|
1. **HeightMap as Universal Canvas**: All procedural data flows through HeightMap objects. Noise and BSP structures generate data *onto* HeightMaps, and Grids/Layers consume data *from* HeightMaps.
|
|||
|
|
|
|||
|
|
2. **Data Stays in C++**: Python defines rules and configuration; C++ executes batch operations. Map data (potentially 1M+ cells) never crosses the Python/C++ boundary during generation.
|
|||
|
|
|
|||
|
|
3. **Composability**: HeightMaps can be combined (add, multiply, lerp), allowing complex terrain from simple building blocks.
|
|||
|
|
|
|||
|
|
4. **Vector-Based Coordinates**: All positions use `(x, y)` tuples. Regions use `((x, y), (w, h))` for bounds or `((x1, y1), (x2, y2))` for world regions.
|
|||
|
|
|
|||
|
|
5. **Mutation with Chaining**: Operations mutate in place and return `self` for method chaining. Threshold operations return new HeightMaps to preserve originals.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architecture
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ SOURCES │
|
|||
|
|
│ │
|
|||
|
|
│ NoiseSource BSP │
|
|||
|
|
│ (infinite function, (structural partition, │
|
|||
|
|
│ stateless) tree of nodes) │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ .sample() │ .to_heightmap() │
|
|||
|
|
│ ▼ ▼ │
|
|||
|
|
│ ┌─────────────┐ ┌─────────────┐ │
|
|||
|
|
│ │ NoiseSample │ │ BSPMap │ │
|
|||
|
|
│ │ (HeightMap) │ │ (HeightMap) │ │
|
|||
|
|
│ └──────┬──────┘ └──────┬──────┘ │
|
|||
|
|
│ │ ▲ │ ▲ │
|
|||
|
|
│ │ │ .add_noise() │ │ .add_bsp() │
|
|||
|
|
│ │ │ .multiply_noise() │ │ .multiply_bsp() │
|
|||
|
|
│ ▼ │ ▼ │ │
|
|||
|
|
│ └────┴───────────┬───────────────────┴────┘ │
|
|||
|
|
│ ▼ │
|
|||
|
|
│ ┌─────────────────┐ │
|
|||
|
|
│ │ HeightMap │ │
|
|||
|
|
│ │ (2D float[]) │ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ Operations: │ │
|
|||
|
|
│ │ • add/multiply │ │
|
|||
|
|
│ │ • hills/erosion │ │
|
|||
|
|
│ │ • threshold │ │
|
|||
|
|
│ │ • normalize │ │
|
|||
|
|
│ └────────┬────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ ┌──────────────┼──────────────┐ │
|
|||
|
|
│ ▼ ▼ ▼ │
|
|||
|
|
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
|||
|
|
│ │ Grid │ │ TileLayer │ │ColorLayer │ │
|
|||
|
|
│ │ │ │ │ │ │ │
|
|||
|
|
│ │ walkable │ │ tile_index│ │ color │ │
|
|||
|
|
│ │transparent│ │ │ │ gradient │ │
|
|||
|
|
│ └───────────┘ └───────────┘ └───────────┘ │
|
|||
|
|
│ TARGETS │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Class Reference
|
|||
|
|
|
|||
|
|
### mcrfpy.HeightMap
|
|||
|
|
|
|||
|
|
The universal canvas for procedural generation. A 2D grid of float values that can be generated, manipulated, combined, and applied to game objects.
|
|||
|
|
|
|||
|
|
#### Constructor
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
HeightMap(size: tuple[int, int], fill: float = 0.0)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `size` | `(int, int)` | Width and height in cells. Immutable after creation. |
|
|||
|
|
| `fill` | `float` | Initial value for all cells. Default 0.0. |
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `size` | `(int, int)` | Read-only. Width and height of the heightmap. |
|
|||
|
|
|
|||
|
|
#### Scalar Operations
|
|||
|
|
|
|||
|
|
All scalar operations mutate in place and return `self` for chaining.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def fill(self, value: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Set all cells to `value`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def clear(self) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Set all cells to 0.0. Equivalent to `fill(0.0)`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_constant(self, value: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Add `value` to every cell.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def scale(self, factor: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Multiply every cell by `factor`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def clamp(self, min: float = 0.0, max: float = 1.0) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Clamp all values to the range [min, max].
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def normalize(self, min: float = 0.0, max: float = 1.0) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Linearly rescale values so the current minimum becomes `min` and current maximum becomes `max`.
|
|||
|
|
|
|||
|
|
#### HeightMap Combination
|
|||
|
|
|
|||
|
|
All combination operations mutate in place and return `self`. The `other` HeightMap must have the same size.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Add `other` to this heightmap cell-by-cell.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def subtract(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Subtract `other` from this heightmap cell-by-cell.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def multiply(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Multiply this heightmap by `other` cell-by-cell. Useful for masking.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def lerp(self, other: HeightMap, t: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Linear interpolation: `self = self + (other - self) * t`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def copy_from(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Copy all values from `other` into this heightmap.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def max(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Per-cell maximum of this and `other`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def min(self, other: HeightMap) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Per-cell minimum of this and `other`.
|
|||
|
|
|
|||
|
|
#### Direct Source Sampling
|
|||
|
|
|
|||
|
|
These methods sample from sources directly onto the heightmap without intermediate allocation.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_noise(self,
|
|||
|
|
source: NoiseSource,
|
|||
|
|
world_region: tuple = None,
|
|||
|
|
mode: str = "fbm",
|
|||
|
|
octaves: int = 4,
|
|||
|
|
scale: float = 1.0) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Sample noise and add to current values.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `source` | `NoiseSource` | The noise generator to sample from. |
|
|||
|
|
| `world_region` | `((x1,y1), (x2,y2))` | World coordinates to sample. Default: `((0,0), size)`. |
|
|||
|
|
| `mode` | `str` | `"flat"`, `"fbm"`, or `"turbulence"`. Default: `"fbm"`. |
|
|||
|
|
| `octaves` | `int` | Octaves for fbm/turbulence. Default: 4. |
|
|||
|
|
| `scale` | `float` | Multiplier for sampled values. Default: 1.0. |
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def multiply_noise(self, source: NoiseSource, **kwargs) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Sample noise and multiply with current values. Same parameters as `add_noise`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_bsp(self,
|
|||
|
|
bsp: BSP,
|
|||
|
|
select: str = "leaves",
|
|||
|
|
nodes: list[BSPNode] = None,
|
|||
|
|
shrink: int = 0,
|
|||
|
|
value: float = 1.0) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Add BSP node regions to heightmap.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `bsp` | `BSP` | The BSP tree to sample from. |
|
|||
|
|
| `select` | `str` | `"leaves"`, `"all"`, or `"internal"`. Default: `"leaves"`. |
|
|||
|
|
| `nodes` | `list[BSPNode]` | Override: specific nodes only. Default: None (use select). |
|
|||
|
|
| `shrink` | `int` | Pixels to shrink from node bounds. Default: 0. |
|
|||
|
|
| `value` | `float` | Value to add inside selected regions. Default: 1.0. |
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def multiply_bsp(self, bsp: BSP, **kwargs) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Multiply by BSP regions. Same parameters as `add_bsp`.
|
|||
|
|
|
|||
|
|
#### Terrain Generation
|
|||
|
|
|
|||
|
|
These methods implement libtcod heightmap generation algorithms.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_hill(self, center: tuple[int, int], radius: float, height: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Add a hill (half-spheroid) at the specified position.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def dig_hill(self, center: tuple[int, int], radius: float, depth: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Dig a depression. Takes minimum of current value and hill shape (for carving rivers, pits).
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def add_voronoi(self,
|
|||
|
|
num_points: int,
|
|||
|
|
coefficients: tuple[float, ...] = (1.0, -0.5),
|
|||
|
|
seed: int = None) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Add Voronoi diagram values. Coefficients weight distance to Nth-closest point.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def mid_point_displacement(self,
|
|||
|
|
roughness: float = 0.5,
|
|||
|
|
seed: int = None) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Generate fractal terrain using diamond-square algorithm. Roughness should be 0.4-0.6.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def rain_erosion(self,
|
|||
|
|
drops: int,
|
|||
|
|
erosion: float = 0.1,
|
|||
|
|
sedimentation: float = 0.05,
|
|||
|
|
seed: int = None) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Simulate rain erosion. `drops` should be at least `width * height`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def dig_bezier(self,
|
|||
|
|
points: tuple[tuple[int, int], ...],
|
|||
|
|
start_radius: float,
|
|||
|
|
end_radius: float,
|
|||
|
|
start_depth: float,
|
|||
|
|
end_depth: float) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Carve a path along a cubic Bezier curve. Requires exactly 4 control points.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def smooth(self, iterations: int = 1) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Apply smoothing kernel. Each iteration averages cells with neighbors.
|
|||
|
|
|
|||
|
|
#### Threshold Operations
|
|||
|
|
|
|||
|
|
These methods return **new** HeightMap objects, preserving the original.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def threshold(self, range: tuple[float, float]) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Return new HeightMap with original values where in range, 0.0 elsewhere.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def threshold_binary(self,
|
|||
|
|
range: tuple[float, float],
|
|||
|
|
value: float = 1.0) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Return new HeightMap with `value` where in range, 0.0 elsewhere.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def inverse(self) -> HeightMap
|
|||
|
|
```
|
|||
|
|
Return new HeightMap with `(1.0 - value)` for each cell.
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
|
|||
|
|
These methods return values to Python for inspection.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def get(self, pos: tuple[int, int]) -> float
|
|||
|
|
```
|
|||
|
|
Get value at integer coordinates.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def get_interpolated(self, pos: tuple[float, float]) -> float
|
|||
|
|
```
|
|||
|
|
Get bilinearly interpolated value at float coordinates.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def get_slope(self, pos: tuple[int, int]) -> float
|
|||
|
|
```
|
|||
|
|
Get slope (0 to π/2) at position.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def get_normal(self, pos: tuple[int, int]) -> tuple[float, float, float]
|
|||
|
|
```
|
|||
|
|
Get normalized surface normal vector at position.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def min_max(self) -> tuple[float, float]
|
|||
|
|
```
|
|||
|
|
Return (minimum_value, maximum_value) across all cells.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def count_in_range(self, range: tuple[float, float]) -> int
|
|||
|
|
```
|
|||
|
|
Count cells with values in the specified range.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.NoiseSource
|
|||
|
|
|
|||
|
|
A configured noise generator function. Stateless and infinite - the same coordinates always produce the same value.
|
|||
|
|
|
|||
|
|
#### Constructor
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
NoiseSource(dimensions: int = 2,
|
|||
|
|
algorithm: str = "simplex",
|
|||
|
|
hurst: float = 0.5,
|
|||
|
|
lacunarity: float = 2.0,
|
|||
|
|
seed: int = None)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `dimensions` | `int` | 1-4. Number of input dimensions. Default: 2. |
|
|||
|
|
| `algorithm` | `str` | `"simplex"`, `"perlin"`, or `"wavelet"`. Default: `"simplex"`. |
|
|||
|
|
| `hurst` | `float` | Fractal Hurst exponent for fbm/turbulence. Default: 0.5. |
|
|||
|
|
| `lacunarity` | `float` | Frequency multiplier between octaves. Default: 2.0. |
|
|||
|
|
| `seed` | `int` | Random seed. None for random. |
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `dimensions` | `int` | Read-only. |
|
|||
|
|
| `algorithm` | `str` | Read-only. |
|
|||
|
|
| `hurst` | `float` | Read-only. |
|
|||
|
|
| `lacunarity` | `float` | Read-only. |
|
|||
|
|
| `seed` | `int` | Read-only. |
|
|||
|
|
|
|||
|
|
#### Point Queries
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def get(self, pos: tuple[float, ...]) -> float
|
|||
|
|
```
|
|||
|
|
Get flat noise value at coordinates. Tuple length must match dimensions. Returns -1.0 to 1.0.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def fbm(self, pos: tuple[float, ...], octaves: int = 4) -> float
|
|||
|
|
```
|
|||
|
|
Get fractal brownian motion value. Returns -1.0 to 1.0.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def turbulence(self, pos: tuple[float, ...], octaves: int = 4) -> float
|
|||
|
|
```
|
|||
|
|
Get turbulence (absolute fbm) value. Returns -1.0 to 1.0.
|
|||
|
|
|
|||
|
|
#### Batch Sampling
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def sample(self,
|
|||
|
|
size: tuple[int, int],
|
|||
|
|
world_region: tuple = None,
|
|||
|
|
mode: str = "fbm",
|
|||
|
|
octaves: int = 4) -> NoiseSample
|
|||
|
|
```
|
|||
|
|
Create a NoiseSample (HeightMap subclass) by sampling a region.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `size` | `(int, int)` | Output dimensions in cells. |
|
|||
|
|
| `world_region` | `((x1,y1), (x2,y2))` | World coordinates to sample. Default: `((0,0), size)`. |
|
|||
|
|
| `mode` | `str` | `"flat"`, `"fbm"`, or `"turbulence"`. Default: `"fbm"`. |
|
|||
|
|
| `octaves` | `int` | Octaves for fbm/turbulence. Default: 4. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.NoiseSample
|
|||
|
|
|
|||
|
|
A HeightMap created by sampling a NoiseSource. Tracks its origin for convenient adjacent sampling and rescaling.
|
|||
|
|
|
|||
|
|
#### Inheritance
|
|||
|
|
|
|||
|
|
`NoiseSample` extends `HeightMap` - all HeightMap methods are available.
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `source` | `NoiseSource` | Read-only. The generator that created this sample. |
|
|||
|
|
| `world_region` | `((x1,y1), (x2,y2))` | Read-only. World coordinates that were sampled. |
|
|||
|
|
| `mode` | `str` | Read-only. `"flat"`, `"fbm"`, or `"turbulence"`. |
|
|||
|
|
| `octaves` | `int` | Read-only. Octaves used for sampling. |
|
|||
|
|
|
|||
|
|
#### Adjacent Sampling
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def next_left(self) -> NoiseSample
|
|||
|
|
def next_right(self) -> NoiseSample
|
|||
|
|
def next_up(self) -> NoiseSample
|
|||
|
|
def next_down(self) -> NoiseSample
|
|||
|
|
```
|
|||
|
|
Sample the adjacent region in the specified direction. Returns a new NoiseSample with the same size, mode, and octaves, but shifted world_region.
|
|||
|
|
|
|||
|
|
#### Rescaling
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def resample(self, size: tuple[int, int]) -> NoiseSample
|
|||
|
|
```
|
|||
|
|
Resample the same world_region at a different output resolution. Useful for minimaps (smaller size) or detail views (larger size).
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def zoom(self, factor: float) -> NoiseSample
|
|||
|
|
```
|
|||
|
|
Sample a different-sized world region at the same output size.
|
|||
|
|
- `factor > 1.0`: Zoom in (smaller world region, more detail)
|
|||
|
|
- `factor < 1.0`: Zoom out (larger world region, overview)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.BSP
|
|||
|
|
|
|||
|
|
Binary space partition tree for rectangular regions. Useful for dungeon rooms, zones, or any hierarchical spatial organization.
|
|||
|
|
|
|||
|
|
#### Constructor
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
BSP(bounds: tuple[tuple[int, int], tuple[int, int]])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `bounds` | `((x, y), (w, h))` | Position and size of the root region. |
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `bounds` | `((x, y), (w, h))` | Read-only. Root node bounds. |
|
|||
|
|
| `root` | `BSPNode` | Read-only. Reference to the root node. |
|
|||
|
|
|
|||
|
|
#### Splitting
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def split_once(self, horizontal: bool, position: int) -> BSP
|
|||
|
|
```
|
|||
|
|
Split the root node once. Returns self for chaining.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def split_recursive(self,
|
|||
|
|
depth: int,
|
|||
|
|
min_size: tuple[int, int],
|
|||
|
|
max_ratio: float = 1.5,
|
|||
|
|
seed: int = None) -> BSP
|
|||
|
|
```
|
|||
|
|
Recursively split to the specified depth.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `depth` | `int` | Maximum recursion depth. Creates up to 2^depth leaves. |
|
|||
|
|
| `min_size` | `(int, int)` | Minimum (width, height) for a node to be split. |
|
|||
|
|
| `max_ratio` | `float` | Maximum aspect ratio before forcing split direction. Default: 1.5. |
|
|||
|
|
| `seed` | `int` | Random seed. None for random. |
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def clear(self) -> BSP
|
|||
|
|
```
|
|||
|
|
Remove all children, keeping only the root node with original bounds.
|
|||
|
|
|
|||
|
|
#### Iteration
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def leaves(self) -> Iterator[BSPNode]
|
|||
|
|
```
|
|||
|
|
Iterate all leaf nodes (the actual rooms).
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def traverse(self, order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]
|
|||
|
|
```
|
|||
|
|
Iterate all nodes in the specified order.
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def find(self, pos: tuple[int, int]) -> BSPNode
|
|||
|
|
```
|
|||
|
|
Find the smallest (deepest) node containing the position.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def find_path(self, start: BSPNode, end: BSPNode) -> tuple[BSPNode, ...]
|
|||
|
|
```
|
|||
|
|
Find a sequence of sibling-connected nodes between start and end leaves. Useful for corridor generation.
|
|||
|
|
|
|||
|
|
#### HeightMap Generation
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def to_heightmap(self,
|
|||
|
|
size: tuple[int, int] = None,
|
|||
|
|
select: str = "leaves",
|
|||
|
|
nodes: list[BSPNode] = None,
|
|||
|
|
shrink: int = 0,
|
|||
|
|
value: float = 1.0) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Create a BSPMap (HeightMap subclass) from selected nodes.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `size` | `(int, int)` | Output size. Default: bounds size. |
|
|||
|
|
| `select` | `str` | `"leaves"`, `"all"`, or `"internal"`. Default: `"leaves"`. |
|
|||
|
|
| `nodes` | `list[BSPNode]` | Override: specific nodes only. |
|
|||
|
|
| `shrink` | `int` | Pixels to shrink from each node's bounds. Default: 0. |
|
|||
|
|
| `value` | `float` | Value inside selected regions. Default: 1.0. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.BSPNode
|
|||
|
|
|
|||
|
|
A lightweight reference to a node in a BSP tree. Read-only.
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `bounds` | `((x, y), (w, h))` | Position and size of this node. |
|
|||
|
|
| `level` | `int` | Depth in tree (0 for root). |
|
|||
|
|
| `index` | `int` | Unique index within the tree. |
|
|||
|
|
| `is_leaf` | `bool` | True if this node has no children. |
|
|||
|
|
| `split_horizontal` | `bool \| None` | Split orientation. None if leaf. |
|
|||
|
|
| `split_position` | `int \| None` | Split coordinate. None if leaf. |
|
|||
|
|
|
|||
|
|
#### Navigation
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `left` | `BSPNode \| None` | Left child, or None if leaf. |
|
|||
|
|
| `right` | `BSPNode \| None` | Right child, or None if leaf. |
|
|||
|
|
| `parent` | `BSPNode \| None` | Parent node, or None if root. |
|
|||
|
|
| `sibling` | `BSPNode \| None` | Other child of parent, or None. |
|
|||
|
|
|
|||
|
|
#### Methods
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def contains(self, pos: tuple[int, int]) -> bool
|
|||
|
|
```
|
|||
|
|
Check if position is inside this node's bounds.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def center(self) -> tuple[int, int]
|
|||
|
|
```
|
|||
|
|
Return the center point of this node's bounds.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.BSPMap
|
|||
|
|
|
|||
|
|
A HeightMap created from BSP node selection. Tracks its origin for convenient re-querying.
|
|||
|
|
|
|||
|
|
#### Inheritance
|
|||
|
|
|
|||
|
|
`BSPMap` extends `HeightMap` - all HeightMap methods are available.
|
|||
|
|
|
|||
|
|
#### Properties
|
|||
|
|
|
|||
|
|
| Property | Type | Description |
|
|||
|
|
|----------|------|-------------|
|
|||
|
|
| `bsp` | `BSP` | Read-only. The tree that created this map. |
|
|||
|
|
| `nodes` | `tuple[BSPNode, ...]` | Read-only. Nodes represented in this map. |
|
|||
|
|
| `shrink` | `int` | Read-only. Margin applied from bounds. |
|
|||
|
|
|
|||
|
|
#### BSP-Specific Operations
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def inverse(self) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Return a new BSPMap with walls instead of rooms (inverts the selection within BSP bounds).
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def expand(self, amount: int = 1) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Return a new BSPMap with node bounds grown by amount.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def contract(self, amount: int = 1) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Return a new BSPMap with node bounds shrunk by additional amount.
|
|||
|
|
|
|||
|
|
#### Re-querying
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def with_nodes(self, nodes: list[BSPNode]) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Return a new BSPMap with different node selection.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def with_shrink(self, shrink: int) -> BSPMap
|
|||
|
|
```
|
|||
|
|
Return a new BSPMap with different shrink value.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### mcrfpy.Traversal
|
|||
|
|
|
|||
|
|
Enumeration for BSP tree traversal orders.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class Traversal(Enum):
|
|||
|
|
PRE_ORDER = "pre" # Node, then left, then right
|
|||
|
|
IN_ORDER = "in" # Left, then node, then right
|
|||
|
|
POST_ORDER = "post" # Left, then right, then node
|
|||
|
|
LEVEL_ORDER = "level" # Top to bottom, left to right
|
|||
|
|
INVERTED_LEVEL_ORDER = "level_inverted" # Bottom to top, right to left
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Application to Grid and Layers
|
|||
|
|
|
|||
|
|
### Grid.apply_threshold
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_threshold(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
range: tuple[float, float],
|
|||
|
|
walkable: bool = None,
|
|||
|
|
transparent: bool = None) -> Grid
|
|||
|
|
```
|
|||
|
|
Set walkable/transparent properties where source value is in range.
|
|||
|
|
|
|||
|
|
| Parameter | Type | Description |
|
|||
|
|
|-----------|------|-------------|
|
|||
|
|
| `source` | `HeightMap` | Must match grid size. |
|
|||
|
|
| `range` | `(float, float)` | (min, max) inclusive range. |
|
|||
|
|
| `walkable` | `bool \| None` | Set walkable to this value. None = don't change. |
|
|||
|
|
| `transparent` | `bool \| None` | Set transparent to this value. None = don't change. |
|
|||
|
|
|
|||
|
|
### Grid.apply_ranges
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_ranges(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
ranges: list[tuple]) -> Grid
|
|||
|
|
```
|
|||
|
|
Apply multiple thresholds in a single pass.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Example
|
|||
|
|
grid.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.3), {"walkable": False, "transparent": True}), # Water
|
|||
|
|
((0.3, 0.8), {"walkable": True, "transparent": True}), # Land
|
|||
|
|
((0.8, 1.0), {"walkable": False, "transparent": False}), # Mountains
|
|||
|
|
])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### TileLayer.apply_threshold
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_threshold(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
range: tuple[float, float],
|
|||
|
|
tile: int) -> TileLayer
|
|||
|
|
```
|
|||
|
|
Set tile index where source value is in range.
|
|||
|
|
|
|||
|
|
### TileLayer.apply_ranges
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_ranges(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
ranges: list[tuple]) -> TileLayer
|
|||
|
|
```
|
|||
|
|
Apply multiple tile assignments in a single pass.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Example
|
|||
|
|
tiles.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.2), DEEP_WATER),
|
|||
|
|
((0.2, 0.3), SHALLOW_WATER),
|
|||
|
|
((0.3, 0.5), SAND),
|
|||
|
|
((0.5, 0.7), GRASS),
|
|||
|
|
((0.7, 0.85), ROCK),
|
|||
|
|
((0.85, 1.0), SNOW),
|
|||
|
|
])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ColorLayer.apply_threshold
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_threshold(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
range: tuple[float, float],
|
|||
|
|
color: tuple[int, ...]) -> ColorLayer
|
|||
|
|
```
|
|||
|
|
Set fixed color where source value is in range. Color is (R, G, B) or (R, G, B, A).
|
|||
|
|
|
|||
|
|
### ColorLayer.apply_gradient
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_gradient(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
range: tuple[float, float],
|
|||
|
|
color_low: tuple[int, ...],
|
|||
|
|
color_high: tuple[int, ...]) -> ColorLayer
|
|||
|
|
```
|
|||
|
|
Interpolate between colors based on source value within range.
|
|||
|
|
- At range minimum → color_low
|
|||
|
|
- At range maximum → color_high
|
|||
|
|
|
|||
|
|
### ColorLayer.apply_ranges
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_ranges(self,
|
|||
|
|
source: HeightMap,
|
|||
|
|
ranges: list[tuple]) -> ColorLayer
|
|||
|
|
```
|
|||
|
|
Apply multiple color assignments. Each range maps to either a fixed color or a gradient.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Example
|
|||
|
|
colors.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.3), (0, 0, 180)), # Fixed blue
|
|||
|
|
((0.3, 0.7), ((50, 120, 50), (100, 200, 100))), # Gradient green (tuple of 2)
|
|||
|
|
((0.7, 1.0), ((100, 100, 100), (255, 255, 255))), # Gradient gray to white
|
|||
|
|
])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Usage Examples
|
|||
|
|
|
|||
|
|
### Example 1: Simple Dungeon
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Create BSP dungeon layout
|
|||
|
|
bsp = mcrfpy.BSP(bounds=((0, 0), (80, 50)))
|
|||
|
|
bsp.split_recursive(depth=4, min_size=(6, 6), max_ratio=1.5)
|
|||
|
|
|
|||
|
|
# Generate room mask (1.0 inside rooms, 0.0 in walls)
|
|||
|
|
rooms = bsp.to_heightmap(select="leaves", shrink=1)
|
|||
|
|
|
|||
|
|
# Apply to grid
|
|||
|
|
grid.apply_threshold(rooms, range=(0.5, 1.0), walkable=True, transparent=True)
|
|||
|
|
|
|||
|
|
# Apply floor tiles
|
|||
|
|
tiles.apply_threshold(rooms, range=(0.5, 1.0), tile=FLOOR_TILE)
|
|||
|
|
|
|||
|
|
# Walls are left as default (not in threshold range)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Example 2: Natural Caves
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Create noise generator
|
|||
|
|
noise = mcrfpy.NoiseSource(algorithm="simplex", seed=42)
|
|||
|
|
|
|||
|
|
# Sample onto heightmap
|
|||
|
|
cave = noise.sample(
|
|||
|
|
size=(100, 100),
|
|||
|
|
world_region=((0, 0), (10, 10)), # 10x zoom for smooth features
|
|||
|
|
mode="fbm",
|
|||
|
|
octaves=4
|
|||
|
|
)
|
|||
|
|
cave.normalize()
|
|||
|
|
|
|||
|
|
# Open areas where noise > 0.45
|
|||
|
|
grid.apply_threshold(cave, range=(0.45, 1.0), walkable=True, transparent=True)
|
|||
|
|
tiles.apply_threshold(cave, range=(0.45, 1.0), tile=CAVE_FLOOR)
|
|||
|
|
tiles.apply_threshold(cave, range=(0.0, 0.45), tile=CAVE_WALL)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Example 3: Overworld with Rivers
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Base terrain
|
|||
|
|
terrain = mcrfpy.HeightMap(size=(200, 200))
|
|||
|
|
terrain.mid_point_displacement(roughness=0.5)
|
|||
|
|
|
|||
|
|
# Add hills
|
|||
|
|
terrain.add_hill(center=(50, 80), radius=20, height=0.4)
|
|||
|
|
terrain.add_hill(center=(150, 120), radius=25, height=0.5)
|
|||
|
|
|
|||
|
|
# Carve river
|
|||
|
|
terrain.dig_bezier(
|
|||
|
|
points=((10, 100), (60, 80), (140, 120), (190, 100)),
|
|||
|
|
start_radius=2, end_radius=5,
|
|||
|
|
start_depth=0.5, end_depth=0.5
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Erosion for realism
|
|||
|
|
terrain.rain_erosion(drops=8000)
|
|||
|
|
terrain.normalize()
|
|||
|
|
|
|||
|
|
# Apply terrain bands
|
|||
|
|
grid.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.25), {"walkable": False, "transparent": True}), # Water
|
|||
|
|
((0.25, 0.75), {"walkable": True, "transparent": True}), # Land
|
|||
|
|
((0.75, 1.0), {"walkable": False, "transparent": False}), # Mountains
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
tiles.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.25), WATER_TILE),
|
|||
|
|
((0.25, 0.4), SAND_TILE),
|
|||
|
|
((0.4, 0.65), GRASS_TILE),
|
|||
|
|
((0.65, 0.8), ROCK_TILE),
|
|||
|
|
((0.8, 1.0), SNOW_TILE),
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
colors.apply_ranges(terrain, [
|
|||
|
|
((0.0, 0.25), ((0, 50, 150), (0, 100, 200))),
|
|||
|
|
((0.25, 0.65), ((80, 160, 80), (120, 200, 120))),
|
|||
|
|
((0.65, 1.0), ((120, 120, 120), (255, 255, 255))),
|
|||
|
|
])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Example 4: Dungeon with Varied Room Terrain
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Create dungeon structure
|
|||
|
|
bsp = mcrfpy.BSP(bounds=((0, 0), (100, 100)))
|
|||
|
|
bsp.split_recursive(depth=5, min_size=(8, 8))
|
|||
|
|
|
|||
|
|
# Room mask
|
|||
|
|
rooms = bsp.to_heightmap(select="leaves", shrink=1)
|
|||
|
|
|
|||
|
|
# Create terrain variation
|
|||
|
|
noise = mcrfpy.NoiseSource(seed=123)
|
|||
|
|
terrain = mcrfpy.HeightMap(size=(100, 100))
|
|||
|
|
terrain.add_noise(noise, mode="fbm", octaves=4)
|
|||
|
|
terrain.normalize()
|
|||
|
|
|
|||
|
|
# Mask terrain to rooms only
|
|||
|
|
terrain.multiply(rooms)
|
|||
|
|
|
|||
|
|
# Apply varied floor tiles within rooms
|
|||
|
|
tiles.apply_threshold(rooms, range=(0.5, 1.0), tile=STONE_FLOOR) # Base floor
|
|||
|
|
tiles.apply_threshold(terrain, range=(0.3, 0.5), tile=MOSSY_FLOOR)
|
|||
|
|
tiles.apply_threshold(terrain, range=(0.5, 0.7), tile=CRACKED_FLOOR)
|
|||
|
|
|
|||
|
|
# Walkability from rooms mask
|
|||
|
|
grid.apply_threshold(rooms, range=(0.5, 1.0), walkable=True, transparent=True)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Example 5: Infinite World with Chunks
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Persistent world generator
|
|||
|
|
WORLD_SEED = 12345
|
|||
|
|
world_noise = mcrfpy.NoiseSource(seed=WORLD_SEED)
|
|||
|
|
|
|||
|
|
CHUNK_SIZE = 64
|
|||
|
|
|
|||
|
|
def generate_chunk(chunk_x: int, chunk_y: int) -> mcrfpy.NoiseSample:
|
|||
|
|
"""Generate terrain for a world chunk."""
|
|||
|
|
return world_noise.sample(
|
|||
|
|
size=(CHUNK_SIZE, CHUNK_SIZE),
|
|||
|
|
world_region=(
|
|||
|
|
(chunk_x * CHUNK_SIZE, chunk_y * CHUNK_SIZE),
|
|||
|
|
((chunk_x + 1) * CHUNK_SIZE, (chunk_y + 1) * CHUNK_SIZE)
|
|||
|
|
),
|
|||
|
|
mode="fbm",
|
|||
|
|
octaves=6
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Generate current chunk
|
|||
|
|
current_chunk = generate_chunk(0, 0)
|
|||
|
|
|
|||
|
|
# When player moves right, get adjacent chunk
|
|||
|
|
next_chunk = current_chunk.next_right()
|
|||
|
|
|
|||
|
|
# Generate minimap of large area
|
|||
|
|
minimap = world_noise.sample(
|
|||
|
|
size=(100, 100),
|
|||
|
|
world_region=((0, 0), (1000, 1000)),
|
|||
|
|
mode="fbm",
|
|||
|
|
octaves=3
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Example 6: Biome Blending
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import mcrfpy
|
|||
|
|
|
|||
|
|
# Multiple noise layers
|
|||
|
|
elevation_noise = mcrfpy.NoiseSource(seed=1)
|
|||
|
|
moisture_noise = mcrfpy.NoiseSource(seed=2)
|
|||
|
|
|
|||
|
|
# Sample both
|
|||
|
|
elevation = mcrfpy.HeightMap(size=(200, 200))
|
|||
|
|
elevation.add_noise(elevation_noise, mode="fbm", octaves=6)
|
|||
|
|
elevation.normalize()
|
|||
|
|
|
|||
|
|
moisture = mcrfpy.HeightMap(size=(200, 200))
|
|||
|
|
moisture.add_noise(moisture_noise, mode="fbm", octaves=4)
|
|||
|
|
moisture.normalize()
|
|||
|
|
|
|||
|
|
# Desert: low moisture AND medium elevation
|
|||
|
|
desert_mask = moisture.threshold((0.0, 0.3))
|
|||
|
|
desert_mask.multiply(elevation.threshold((0.2, 0.6)))
|
|||
|
|
|
|||
|
|
# Forest: high moisture AND low elevation
|
|||
|
|
forest_mask = moisture.threshold((0.6, 1.0))
|
|||
|
|
forest_mask.multiply(elevation.threshold((0.1, 0.5)))
|
|||
|
|
|
|||
|
|
# Swamp: high moisture AND very low elevation
|
|||
|
|
swamp_mask = moisture.threshold((0.7, 1.0))
|
|||
|
|
swamp_mask.multiply(elevation.threshold((0.0, 0.2)))
|
|||
|
|
|
|||
|
|
# Apply biome tiles (later biomes override earlier)
|
|||
|
|
tiles.apply_threshold(elevation, range=(0.0, 1.0), tile=GRASS_TILE) # Default
|
|||
|
|
tiles.apply_threshold(desert_mask, range=(0.5, 1.0), tile=SAND_TILE)
|
|||
|
|
tiles.apply_threshold(forest_mask, range=(0.5, 1.0), tile=TREE_TILE)
|
|||
|
|
tiles.apply_threshold(swamp_mask, range=(0.5, 1.0), tile=SWAMP_TILE)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Implementation Notes
|
|||
|
|
|
|||
|
|
### Size Matching
|
|||
|
|
|
|||
|
|
All HeightMap operations between two heightmaps require matching sizes. The `apply_*` methods on Grid/Layer also require the source HeightMap to match the Grid's `grid_size`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# This will raise ValueError
|
|||
|
|
small = mcrfpy.HeightMap(size=(50, 50))
|
|||
|
|
large = mcrfpy.HeightMap(size=(100, 100))
|
|||
|
|
large.add(small) # Error: size mismatch
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Value Ranges
|
|||
|
|
|
|||
|
|
- NoiseSource outputs: -1.0 to 1.0
|
|||
|
|
- HeightMap after `normalize()`: 0.0 to 1.0 (by default)
|
|||
|
|
- Threshold operations: work on any float range
|
|||
|
|
|
|||
|
|
Recommendation: Always `normalize()` before applying to Grid/Layer for predictable threshold behavior.
|
|||
|
|
|
|||
|
|
### Performance Considerations
|
|||
|
|
|
|||
|
|
For grids up to 1000×1000 (1M cells):
|
|||
|
|
|
|||
|
|
| Operation | Approximate Cost |
|
|||
|
|
|-----------|------------------|
|
|||
|
|
| HeightMap creation | O(n) |
|
|||
|
|
| add/multiply/scale | O(n), parallelizable |
|
|||
|
|
| add_noise | O(n × octaves), parallelizable |
|
|||
|
|
| mid_point_displacement | O(n log n) |
|
|||
|
|
| rain_erosion | O(drops) |
|
|||
|
|
| apply_ranges | O(n × num_ranges), single pass |
|
|||
|
|
|
|||
|
|
All operations execute entirely in C++ with no Python callbacks.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Future Considerations
|
|||
|
|
|
|||
|
|
The following features are explicitly **out of scope** for the initial implementation but should not be precluded by the design:
|
|||
|
|
|
|||
|
|
1. **Serialization**: Saving/loading HeightMaps to disk for pre-generated worlds.
|
|||
|
|
|
|||
|
|
2. **Region Operations**: Applying operations to sub-regions when HeightMap sizes don't match.
|
|||
|
|
|
|||
|
|
3. **Corridor Generation**: Built-in BSP corridor creation (currently user code using `find_path` and `dig_bezier` or manual fills).
|
|||
|
|
|
|||
|
|
4. **Custom Kernels**: User-defined kernel transforms beyond simple smoothing.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix: libtcod Function Mapping
|
|||
|
|
|
|||
|
|
| libtcod Function | McRogueFace Equivalent |
|
|||
|
|
|------------------|------------------------|
|
|||
|
|
| `TCOD_noise_new` | `NoiseSource()` |
|
|||
|
|
| `TCOD_noise_get` | `NoiseSource.get()` |
|
|||
|
|
| `TCOD_noise_get_fbm` | `NoiseSource.fbm()` |
|
|||
|
|
| `TCOD_noise_get_turbulence` | `NoiseSource.turbulence()` |
|
|||
|
|
| `TCOD_heightmap_new` | `HeightMap()` |
|
|||
|
|
| `TCOD_heightmap_add_hill` | `HeightMap.add_hill()` |
|
|||
|
|
| `TCOD_heightmap_dig_hill` | `HeightMap.dig_hill()` |
|
|||
|
|
| `TCOD_heightmap_rain_erosion` | `HeightMap.rain_erosion()` |
|
|||
|
|
| `TCOD_heightmap_add_voronoi` | `HeightMap.add_voronoi()` |
|
|||
|
|
| `TCOD_heightmap_mid_point_displacement` | `HeightMap.mid_point_displacement()` |
|
|||
|
|
| `TCOD_heightmap_dig_bezier` | `HeightMap.dig_bezier()` |
|
|||
|
|
| `TCOD_heightmap_add_fbm` | `HeightMap.add_noise(..., mode="fbm")` |
|
|||
|
|
| `TCOD_heightmap_scale_fbm` | `HeightMap.multiply_noise(..., mode="fbm")` |
|
|||
|
|
| `TCOD_heightmap_normalize` | `HeightMap.normalize()` |
|
|||
|
|
| `TCOD_heightmap_clamp` | `HeightMap.clamp()` |
|
|||
|
|
| `TCOD_heightmap_add` | `HeightMap.add()` |
|
|||
|
|
| `TCOD_heightmap_multiply` | `HeightMap.multiply()` |
|
|||
|
|
| `TCOD_heightmap_lerp` | `HeightMap.lerp()` |
|
|||
|
|
| `TCOD_bsp_new_with_size` | `BSP()` |
|
|||
|
|
| `TCOD_bsp_split_once` | `BSP.split_once()` |
|
|||
|
|
| `TCOD_bsp_split_recursive` | `BSP.split_recursive()` |
|
|||
|
|
| `TCOD_bsp_traverse_*` | `BSP.traverse(order=...)` |
|
|||
|
|
| `TCOD_bsp_is_leaf` | `BSPNode.is_leaf` |
|
|||
|
|
| `TCOD_bsp_contains` | `BSPNode.contains()` |
|
|||
|
|
| `TCOD_bsp_find_node` | `BSP.find()` |
|