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:
@@ -122,6 +122,70 @@ impl TerrainAnalysis {
|
||||
pub fn is_ocean(&self, r: usize, c: usize) -> bool {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user