feat(simulation): Layer-1 topography pipeline — features, sub-biome, orchestrator (#953)

Wire the empty-world topography cascade (D-208/209/210):
- generator.rs: add SubBiomeVariant (11 variants, D-210) + sub_biome and
  terrain_modification_cost fields on GeographicAttractor; AttractorType
  is now Copy.
- features.rs (new, D-209): extract the 7 attractor tags from heightmap +
  drainage. Coast/lake derived from the heightmap (D-209/D-223
  reconciliation — markers are names-only now, no polygons). Deterministic
  (sorted seeds, integer keys, bucket-grid thinning); strength-capped at
  MAX_ATTRACTORS preserving type diversity. Shared TerrainAnalysis
  (masks/slope/moisture/percentile) feeds both extraction and sub-biome.
- subbiome.rs (new, D-210): classify sub-biome + terrain_modification_cost
  from elevation/slope/moisture/latitude. Volcanic stays in the enum but
  is not emitted (no heightmap signal).
- layer1.rs (new): run_layer1 orchestrator + attach_feature_names (D-223
  pool names to largest rivers / Alpine peaks).
- attractor_matching constructors updated for the new fields.

76 atlas tests pass; run_layer1 determinism verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 00:35:39 +02:00
co-authored by Claude Opus 4.7
parent 7cf7614342
commit d418eba8bb
6 changed files with 997 additions and 3 deletions
+29 -2
View File
@@ -371,7 +371,7 @@ pub enum TerritorialStatus {
/// The type of terrain feature that attracts settlement placement.
/// Source: D-195, D-209
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttractorType {
/// Where a river meets sea level or coastline. Historically high-value.
RiverMouth,
@@ -389,8 +389,29 @@ pub enum AttractorType {
PlainCenter,
}
/// Fine-grained terrain classification carried by each `GeographicAttractor`.
/// Classifies the local terrain more finely than the top-level `SettingType`;
/// drives ZonePalette modifier selection (D-101) and `terrain_modification_cost`.
/// Source: D-210
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubBiomeVariant {
TropicalWet,
TemperateForest,
TemperateGrassland,
BorealForest,
Tundra,
Desert,
Savanna,
Alpine,
Wetland,
CoastalLowland,
/// No heightmap-derivable signal in the Layer-1 inputs (elevation, slope,
/// moisture, latitude); reserved for a future volcanic data source (D-210).
Volcanic,
}
/// A terrain feature at a specific map position that influences city placement scoring.
/// Source: D-195, D-209
/// Source: D-195, D-209, D-210
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GeographicAttractor {
/// Pixel position in heightmap space [row, col].
@@ -398,6 +419,12 @@ pub struct GeographicAttractor {
pub attractor_type: AttractorType,
/// Normalized strength 0.01.0. Derived from flow accumulation or habitability score.
pub strength: f32,
/// Fine-grained terrain classification at this position (D-210).
pub sub_biome: SubBiomeVariant,
/// Infrastructure-build cost multiplier (1.0 = baseline grassland; higher =
/// more expensive). Derived from `sub_biome` + local slope. Consumed by the
/// attractor-matching pipeline (D-211) to penalize marginal cities. (D-210)
pub terrain_modification_cost: f32,
}
/// Compatibility weights between economic roles and attractor types.