feat(simulation): Layer-1 water bearing + D-234 waterfront rule (#957)
Close the last D-234 piece — terrain water-direction extraction wired through to founding orientation and the quarter waterfront rule: - Layer 1: TerrainAnalysis::water_bearing — 8-octant integer bearing toward the nearest water from the water_dist gradient (D-010, no atan2). Stored on GeographicAttractor.water_bearing (360 = none). - #956 founding orientation: coastal/river settlements now get a real water-facing bearing (the anchoring attractor's), replacing the 0 stub. - #957 waterfront rule (D-234b): the water-facing quarter edge (from the settlement's Coastal founding orientation) drops its block setback to 0 so buildings present flush to the quay (dock-orthogonal). Typed Edge + coastal_edge + per-block gating. Golden + atlas_response fixture rebaked (additive water_bearing field only). 8 new tests. All integer-deterministic (D-010). Pending: the waterfront rule reads context.founding_orientation, which city_context_reader still stubs to Cardinal — real per-settlement orientation reaches quarter generation once the Layer-3 placement -> Layer-4 GenerateSkeleton dispatch is wired (the remaining cross-layer integration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
use crate::atlas::features::NO_WATER_BEARING;
|
||||||
use crate::seed::{splitmix64, SeedChain};
|
use crate::seed::{splitmix64, SeedChain};
|
||||||
use crate::simulation::generator::{
|
use crate::simulation::generator::{
|
||||||
AttractorType, CompatibilityMatrix, GeographicAttractor, SettlementClass, SubBiomeVariant,
|
AttractorType, CompatibilityMatrix, GeographicAttractor, SettlementClass, SubBiomeVariant,
|
||||||
@@ -265,6 +266,7 @@ fn synthetic_attractor(placed: &[CityPlacement], grid_w: u32, grid_h: u32) -> Ge
|
|||||||
strength: 50,
|
strength: 50,
|
||||||
sub_biome: SubBiomeVariant::TemperateGrassland,
|
sub_biome: SubBiomeVariant::TemperateGrassland,
|
||||||
terrain_modification_cost: 100,
|
terrain_modification_cost: 100,
|
||||||
|
water_bearing: NO_WATER_BEARING, // inland synthetic — no water direction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +284,7 @@ fn synthetic_attractor(placed: &[CityPlacement], grid_w: u32, grid_h: u32) -> Ge
|
|||||||
/// grids vary per seed (D-213) while staying deterministic (D-010).
|
/// grids vary per seed (D-213) while staying deterministic (D-010).
|
||||||
fn city_character(
|
fn city_character(
|
||||||
attractor_type: AttractorType,
|
attractor_type: AttractorType,
|
||||||
|
water_bearing: u16,
|
||||||
economic_role: &str,
|
economic_role: &str,
|
||||||
territorial_status: &TerritorialStatus,
|
territorial_status: &TerritorialStatus,
|
||||||
city_id: u64,
|
city_id: u64,
|
||||||
@@ -290,9 +293,15 @@ fn city_character(
|
|||||||
let archetype = political_archetype(territorial_status, economic_role);
|
let archetype = political_archetype(territorial_status, economic_role);
|
||||||
let pattern = arrangement_pattern(&archetype, economic_role);
|
let pattern = arrangement_pattern(&archetype, economic_role);
|
||||||
let free_bearing = (splitmix64(seed.seed() ^ city_id) % 360) as u16;
|
let free_bearing = (splitmix64(seed.seed() ^ city_id) % 360) as u16;
|
||||||
// river_bearing / coastal_facing are 0 until Layer 1 exposes terrain bearings
|
// Layer-1 water bearing (#957) drives both the coastal facing and the river
|
||||||
// (D-209 follow-on); free_bearing seeds the pioneer/open-terrain case.
|
// bearing (the anchoring attractor's direction-to-water); 360 = none → 0.
|
||||||
let orientation = founding_orientation(&attractor_type, territorial_status, 0, 0, free_bearing);
|
let wb = if water_bearing >= 360 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
water_bearing
|
||||||
|
};
|
||||||
|
let orientation =
|
||||||
|
founding_orientation(&attractor_type, territorial_status, wb, wb, free_bearing);
|
||||||
(archetype, pattern, orientation)
|
(archetype, pattern, orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +363,7 @@ pub fn match_cities(
|
|||||||
flag_mismatch(&cities[ci].name, score);
|
flag_mismatch(&cities[ci].name, score);
|
||||||
let (archetype, pattern, orientation) = city_character(
|
let (archetype, pattern, orientation) = city_character(
|
||||||
attractors[ai].attractor_type,
|
attractors[ai].attractor_type,
|
||||||
|
attractors[ai].water_bearing,
|
||||||
&cities[ci].economic_role,
|
&cities[ci].economic_role,
|
||||||
territorial_status,
|
territorial_status,
|
||||||
cities[ci].city_id,
|
cities[ci].city_id,
|
||||||
@@ -420,6 +430,7 @@ pub fn match_cities(
|
|||||||
flag_mismatch(&cities[ci].name, score);
|
flag_mismatch(&cities[ci].name, score);
|
||||||
let (archetype, pattern, orientation) = city_character(
|
let (archetype, pattern, orientation) = city_character(
|
||||||
attractors[ai].attractor_type,
|
attractors[ai].attractor_type,
|
||||||
|
attractors[ai].water_bearing,
|
||||||
&cities[ci].economic_role,
|
&cities[ci].economic_role,
|
||||||
territorial_status,
|
territorial_status,
|
||||||
cities[ci].city_id,
|
cities[ci].city_id,
|
||||||
@@ -453,6 +464,7 @@ pub fn match_cities(
|
|||||||
flag_mismatch(&city.name, score);
|
flag_mismatch(&city.name, score);
|
||||||
let (archetype, pattern, orientation) = city_character(
|
let (archetype, pattern, orientation) = city_character(
|
||||||
AttractorType::PlainCenter,
|
AttractorType::PlainCenter,
|
||||||
|
synthetic.water_bearing,
|
||||||
&city.economic_role,
|
&city.economic_role,
|
||||||
territorial_status,
|
territorial_status,
|
||||||
city.city_id,
|
city.city_id,
|
||||||
@@ -659,6 +671,7 @@ mod tests {
|
|||||||
strength,
|
strength,
|
||||||
sub_biome: SubBiomeVariant::TemperateGrassland,
|
sub_biome: SubBiomeVariant::TemperateGrassland,
|
||||||
terrain_modification_cost: 100,
|
terrain_modification_cost: 100,
|
||||||
|
water_bearing: NO_WATER_BEARING,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,70 @@ impl TerrainAnalysis {
|
|||||||
pub fn is_ocean(&self, r: usize, c: usize) -> bool {
|
pub fn is_ocean(&self, r: usize, c: usize) -> bool {
|
||||||
self.ocean_mask[idx(r, c, self.w)]
|
self.ocean_mask[idx(r, c, self.w)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compass bearing toward the nearest water from cell `(r, c)`, quantized to
|
||||||
|
/// 8 octants (0=N, 45=NE … 315=NW); `360` = "no water in range" (#957, D-234).
|
||||||
|
///
|
||||||
|
/// Reads the `water_dist` field's local gradient — the 8-neighbour with the
|
||||||
|
/// smallest distance-to-water points toward water. Integer-only (no `atan2`)
|
||||||
|
/// for D-010 determinism. Returns `360` when the cell is itself water or no
|
||||||
|
/// neighbour is closer to water (flat/inland).
|
||||||
|
pub fn water_bearing(&self, r: usize, c: usize) -> u16 {
|
||||||
|
let here = self.water_dist[idx(r, c, self.w)];
|
||||||
|
if here == 0 || here >= WATER_DIST_CAP {
|
||||||
|
return NO_WATER_BEARING; // on water, or no water within range
|
||||||
|
}
|
||||||
|
let mut best = here;
|
||||||
|
let mut bdir = (0i32, 0i32);
|
||||||
|
for &(dr, dc) in &NB8 {
|
||||||
|
let nr = r as i32 + dr;
|
||||||
|
if nr < 0 || nr >= self.h as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let nc = wrap_col(c as i32 + dc, self.w as i32);
|
||||||
|
let nd = self.water_dist[idx(nr as usize, nc, self.w)];
|
||||||
|
if nd < best {
|
||||||
|
best = nd;
|
||||||
|
bdir = (dr, dc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bdir == (0, 0) {
|
||||||
|
NO_WATER_BEARING
|
||||||
|
} else {
|
||||||
|
octant_bearing(bdir.0, bdir.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sentinel for [`TerrainAnalysis::water_bearing`] meaning "no water direction".
|
||||||
|
pub const NO_WATER_BEARING: u16 = 360;
|
||||||
|
|
||||||
|
/// Quantize a (Δrow, Δcol) step to a compass octant bearing (0=N … 315=NW).
|
||||||
|
/// `Δrow < 0` is north (rows increase downward). Integer-only (D-010).
|
||||||
|
fn octant_bearing(drow: i32, dcol: i32) -> u16 {
|
||||||
|
let (ar, ac) = (drow.abs(), dcol.abs());
|
||||||
|
let north = drow < 0;
|
||||||
|
let east = dcol > 0;
|
||||||
|
if ar >= ac * 2 {
|
||||||
|
if north {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
180
|
||||||
|
}
|
||||||
|
} else if ac >= ar * 2 {
|
||||||
|
if east {
|
||||||
|
90
|
||||||
|
} else {
|
||||||
|
270
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match (north, east) {
|
||||||
|
(true, true) => 45,
|
||||||
|
(true, false) => 315,
|
||||||
|
(false, true) => 135,
|
||||||
|
(false, false) => 225,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Largest connected below-sea-level component = ocean; all others = lakes.
|
/// Largest connected below-sea-level component = ocean; all others = lakes.
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ pub fn run_layer1(hm: &BodyHeightmap) -> Layer1Output {
|
|||||||
strength: r.strength,
|
strength: r.strength,
|
||||||
sub_biome,
|
sub_biome,
|
||||||
terrain_modification_cost,
|
terrain_modification_cost,
|
||||||
|
// Layer-1 water-direction extraction (#957, D-234) — feeds D-213
|
||||||
|
// founding orientation + the D-234 waterfront rule.
|
||||||
|
water_bearing: ta.water_bearing(r.row as usize, r.col as usize),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ use crate::simulation::generator::{
|
|||||||
AccessKind, AccessPoint, ArchitectureFlavorRef, BlockPlacement, BlockSkeleton,
|
AccessKind, AccessPoint, ArchitectureFlavorRef, BlockPlacement, BlockSkeleton,
|
||||||
BuildingEntryClass, BuildingPropertyTag, BulkClass, ChunkLayout, CityGenerationContext,
|
BuildingEntryClass, BuildingPropertyTag, BulkClass, ChunkLayout, CityGenerationContext,
|
||||||
ComplexityTier, ConstructionEra, CorridorSpine, DistrictLayoutMode, DistrictType, EraCause,
|
ComplexityTier, ConstructionEra, CorridorSpine, DistrictLayoutMode, DistrictType, EraCause,
|
||||||
FloorExtent, FloorHeightProfile, MorphologyZone, MultiBlockReservation, PoliticalArchetype,
|
FloorExtent, FloorHeightProfile, FoundingOrientation, MorphologyZone, MultiBlockReservation,
|
||||||
QuarterId, QuarterSkeleton, ReservationFunction, ReservationId, SettingType, TileRect,
|
PoliticalArchetype, QuarterId, QuarterSkeleton, ReservationFunction, ReservationId,
|
||||||
WorldTier, ZoneTypeId, ZoningType,
|
SettingType, TileRect, WorldTier, ZoneTypeId, ZoningType,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -752,17 +752,54 @@ fn bsp(rect: TileRect, depth: u8, min_lot: u8, max_lot: u8, seed: u64, out: &mut
|
|||||||
bsp(b, depth + 1, min_lot, max_lot, splitmix64(child), out);
|
bsp(b, depth + 1, min_lot, max_lot, splitmix64(child), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subdivide one block into building footprints (D-220/D-229/D-233).
|
/// A quarter (or block) edge — used for the D-234 waterfront rule.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Edge {
|
||||||
|
North,
|
||||||
|
East,
|
||||||
|
South,
|
||||||
|
West,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The quarter edge facing water, from the settlement's founding orientation
|
||||||
|
/// (D-234b). Only `Coastal` orientations have a water-facing edge; the 8-octant
|
||||||
|
/// facing snaps to the nearest cardinal edge.
|
||||||
|
fn coastal_edge(orientation: &FoundingOrientation) -> Option<Edge> {
|
||||||
|
if let FoundingOrientation::Coastal { facing_degrees } = orientation {
|
||||||
|
Some(match facing_degrees % 360 {
|
||||||
|
d if !(45..315).contains(&d) => Edge::North,
|
||||||
|
d if d < 135 => Edge::East,
|
||||||
|
d if d < 225 => Edge::South,
|
||||||
|
_ => Edge::West,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether block `(row, col)` sits on the quarter's `edge` (4×4 grid).
|
||||||
|
fn block_on_quarter_edge(row: u8, col: u8, edge: Edge) -> bool {
|
||||||
|
match edge {
|
||||||
|
Edge::North => row == 0,
|
||||||
|
Edge::South => row == 3,
|
||||||
|
Edge::West => col == 0,
|
||||||
|
Edge::East => col == 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subdivide one block into building footprints (D-220/D-229/D-233/D-234).
|
||||||
///
|
///
|
||||||
/// Lot size + setback scale with `density_pct` (Frontier → few big lots, wide
|
/// Lot size + setback scale with `density_pct` (Frontier → few big lots, wide
|
||||||
/// gaps; Compressed → many small lots, shared walls). The D-233 roofed coverage
|
/// gaps; Compressed → many small lots, shared walls). The D-233 roofed coverage
|
||||||
/// (from `bulk`) decides what fraction of lots are buildings vs interstitial
|
/// (from `bulk`) decides what fraction of lots are buildings vs interstitial
|
||||||
/// open space (yards/parks/lots). Footprints are axis-aligned `TileRect`s in
|
/// open space (yards/parks/lots). Footprints are axis-aligned `TileRect`s in
|
||||||
/// block tile-space.
|
/// block tile-space. `waterfront` (D-234b): on the water-facing edge the street
|
||||||
|
/// margin drops to 0 — buildings present flush to the quay (dock-orthogonal).
|
||||||
fn subdivide_block_footprints(
|
fn subdivide_block_footprints(
|
||||||
density_pct: u8,
|
density_pct: u8,
|
||||||
bulk: &BulkClass,
|
bulk: &BulkClass,
|
||||||
_morphology: &MorphologyZone,
|
_morphology: &MorphologyZone,
|
||||||
|
waterfront: Option<Edge>,
|
||||||
seed: SeedChain,
|
seed: SeedChain,
|
||||||
) -> Vec<TileRect> {
|
) -> Vec<TileRect> {
|
||||||
let (min_lot, max_lot, setback) = match density_pct {
|
let (min_lot, max_lot, setback) = match density_pct {
|
||||||
@@ -774,11 +811,21 @@ fn subdivide_block_footprints(
|
|||||||
};
|
};
|
||||||
let coverage = roofed_coverage_pct(density_pct, bulk);
|
let coverage = roofed_coverage_pct(density_pct, bulk);
|
||||||
|
|
||||||
|
// Perimeter street margin — dropped to 0 on a water-facing edge (D-234b).
|
||||||
|
let (mut top, mut bottom, mut left, mut right) =
|
||||||
|
(BLOCK_MARGIN, BLOCK_MARGIN, BLOCK_MARGIN, BLOCK_MARGIN);
|
||||||
|
match waterfront {
|
||||||
|
Some(Edge::North) => top = 0,
|
||||||
|
Some(Edge::South) => bottom = 0,
|
||||||
|
Some(Edge::West) => left = 0,
|
||||||
|
Some(Edge::East) => right = 0,
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
let inner = TileRect::new(
|
let inner = TileRect::new(
|
||||||
BLOCK_MARGIN,
|
left,
|
||||||
BLOCK_MARGIN,
|
top,
|
||||||
BLOCK_TILES - 2 * BLOCK_MARGIN,
|
BLOCK_TILES - left - right,
|
||||||
BLOCK_TILES - 2 * BLOCK_MARGIN,
|
BLOCK_TILES - top - bottom,
|
||||||
);
|
);
|
||||||
let mut leaves = Vec::new();
|
let mut leaves = Vec::new();
|
||||||
bsp(inner, 0, min_lot, max_lot, seed.seed(), &mut leaves);
|
bsp(inner, 0, min_lot, max_lot, seed.seed(), &mut leaves);
|
||||||
@@ -808,6 +855,7 @@ fn assign_block_tags(
|
|||||||
context: &CityGenerationContext,
|
context: &CityGenerationContext,
|
||||||
economic_role: &str,
|
economic_role: &str,
|
||||||
founding_age_years: u32,
|
founding_age_years: u32,
|
||||||
|
waterfront: Option<Edge>,
|
||||||
block_chain: SeedChain,
|
block_chain: SeedChain,
|
||||||
) -> Vec<BuildingPropertyTag> {
|
) -> Vec<BuildingPropertyTag> {
|
||||||
if block.reservation.is_some() {
|
if block.reservation.is_some() {
|
||||||
@@ -821,6 +869,7 @@ fn assign_block_tags(
|
|||||||
block.density_pct,
|
block.density_pct,
|
||||||
&context.dominant_bulk_class,
|
&context.dominant_bulk_class,
|
||||||
&context.morphology_zone,
|
&context.morphology_zone,
|
||||||
|
waterfront,
|
||||||
block_chain,
|
block_chain,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -873,17 +922,22 @@ pub fn assign_all_block_tags(
|
|||||||
founding_age_years: u32,
|
founding_age_years: u32,
|
||||||
chain: SeedChain,
|
chain: SeedChain,
|
||||||
) -> BTreeMap<(u8, u8), Vec<BuildingPropertyTag>> {
|
) -> BTreeMap<(u8, u8), Vec<BuildingPropertyTag>> {
|
||||||
|
// Water-facing quarter edge from the settlement's coastal founding
|
||||||
|
// orientation (D-234b); blocks on it present flush to the quay.
|
||||||
|
let water_edge = coastal_edge(&context.founding_orientation);
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
for row in 0..4u8 {
|
for row in 0..4u8 {
|
||||||
for col in 0..4u8 {
|
for col in 0..4u8 {
|
||||||
let block = &skeleton.blocks[row as usize][col as usize];
|
let block = &skeleton.blocks[row as usize][col as usize];
|
||||||
let block_chain = chain.derive(SeedDomain::Block, (row * 4 + col) as u64);
|
let block_chain = chain.derive(SeedDomain::Block, (row * 4 + col) as u64);
|
||||||
|
let waterfront = water_edge.filter(|&e| block_on_quarter_edge(row, col, e));
|
||||||
let tags = assign_block_tags(
|
let tags = assign_block_tags(
|
||||||
block,
|
block,
|
||||||
skeleton,
|
skeleton,
|
||||||
context,
|
context,
|
||||||
economic_role,
|
economic_role,
|
||||||
founding_age_years,
|
founding_age_years,
|
||||||
|
waterfront,
|
||||||
block_chain,
|
block_chain,
|
||||||
);
|
);
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
@@ -1523,6 +1577,7 @@ mod tests {
|
|||||||
70,
|
70,
|
||||||
&BulkClass::NonPhysical,
|
&BulkClass::NonPhysical,
|
||||||
&MorphologyZone::AlluvialPlain,
|
&MorphologyZone::AlluvialPlain,
|
||||||
|
None,
|
||||||
SeedChain::root(1),
|
SeedChain::root(1),
|
||||||
);
|
);
|
||||||
assert!(!fps.is_empty(), "a dense block should produce footprints");
|
assert!(!fps.is_empty(), "a dense block should produce footprints");
|
||||||
@@ -1539,12 +1594,14 @@ mod tests {
|
|||||||
15,
|
15,
|
||||||
&BulkClass::NonPhysical,
|
&BulkClass::NonPhysical,
|
||||||
&MorphologyZone::AlluvialPlain,
|
&MorphologyZone::AlluvialPlain,
|
||||||
|
None,
|
||||||
SeedChain::root(7),
|
SeedChain::root(7),
|
||||||
);
|
);
|
||||||
let dense = subdivide_block_footprints(
|
let dense = subdivide_block_footprints(
|
||||||
85,
|
85,
|
||||||
&BulkClass::NonPhysical,
|
&BulkClass::NonPhysical,
|
||||||
&MorphologyZone::AlluvialPlain,
|
&MorphologyZone::AlluvialPlain,
|
||||||
|
None,
|
||||||
SeedChain::root(7),
|
SeedChain::root(7),
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1676,6 +1733,42 @@ mod tests {
|
|||||||
assert!(corridors.iter().all(|c| c.from == hub || c.to == hub));
|
assert!(corridors.iter().all(|c| c.from == hub || c.to == hub));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coastal_edge_maps_facing_to_cardinal() {
|
||||||
|
let c = |d| coastal_edge(&FoundingOrientation::Coastal { facing_degrees: d });
|
||||||
|
assert_eq!(c(0), Some(Edge::North));
|
||||||
|
assert_eq!(c(90), Some(Edge::East));
|
||||||
|
assert_eq!(c(180), Some(Edge::South));
|
||||||
|
assert_eq!(c(270), Some(Edge::West));
|
||||||
|
assert_eq!(coastal_edge(&FoundingOrientation::Cardinal), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn waterfront_footprints_present_to_the_quay() {
|
||||||
|
// The water-facing (north) edge drops its setback → buildings sit flush
|
||||||
|
// (origin.1 == 0), unlike the standard margin (D-234b).
|
||||||
|
let inland = subdivide_block_footprints(
|
||||||
|
70,
|
||||||
|
&BulkClass::NonPhysical,
|
||||||
|
&MorphologyZone::CoastalLowland,
|
||||||
|
None,
|
||||||
|
SeedChain::root(3),
|
||||||
|
);
|
||||||
|
let quay = subdivide_block_footprints(
|
||||||
|
70,
|
||||||
|
&BulkClass::NonPhysical,
|
||||||
|
&MorphologyZone::CoastalLowland,
|
||||||
|
Some(Edge::North),
|
||||||
|
SeedChain::root(3),
|
||||||
|
);
|
||||||
|
let min_y = |v: &[TileRect]| v.iter().map(|r| r.origin.1).min().unwrap_or(u8::MAX);
|
||||||
|
assert!(min_y(&inland) >= BLOCK_MARGIN);
|
||||||
|
assert!(
|
||||||
|
min_y(&quay) < min_y(&inland),
|
||||||
|
"quay buildings should reach the water edge"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn skeleton_has_streets_and_local_lattice() {
|
fn skeleton_has_streets_and_local_lattice() {
|
||||||
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
|
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
|
||||||
|
|||||||
@@ -529,6 +529,12 @@ pub struct GeographicAttractor {
|
|||||||
/// `sub_biome` + local slope; penalizes marginal cities in the matching
|
/// `sub_biome` + local slope; penalizes marginal cities in the matching
|
||||||
/// pipeline (D-211). Integer for D-010 determinism (#955). (D-210)
|
/// pipeline (D-211). Integer for D-010 determinism (#955). (D-210)
|
||||||
pub terrain_modification_cost: i32,
|
pub terrain_modification_cost: i32,
|
||||||
|
/// Compass bearing (degrees, quantized to 8 octants: 0=N,45=NE…315=NW)
|
||||||
|
/// toward the nearest water from this position, or 360 = "no water nearby".
|
||||||
|
/// Layer-1 terrain extraction feeding D-213 founding orientation (coastal
|
||||||
|
/// facing / river bearing) and the D-234 waterfront rule. Integer 8-octant
|
||||||
|
/// keeps it D-010-deterministic (no `atan2`).
|
||||||
|
pub water_bearing: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compatibility weights between economic roles and attractor types.
|
/// Compatibility weights between economic roles and attractor types.
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ fn generate_atlas_layer_response_fixtures() {
|
|||||||
strength: 90,
|
strength: 90,
|
||||||
sub_biome: SubBiomeVariant::CoastalLowland,
|
sub_biome: SubBiomeVariant::CoastalLowland,
|
||||||
terrain_modification_cost: 170,
|
terrain_modification_cost: 170,
|
||||||
|
water_bearing: 90,
|
||||||
}],
|
}],
|
||||||
grid_w: 512,
|
grid_w: 512,
|
||||||
grid_h: 256,
|
grid_h: 256,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user