Commit graph

49 commits

Author SHA1 Message Date
6c318e4df0 feat: offshore islands (terrain-water task 11)
Sparse discoverable islets from a low-frequency ocean noise layer, seeded
resolved+7607 at 14 undulations/island (~585 px blobs).

Placed by LERPING the seabed toward a target crest (34 m raw above sea)
rather than adding to it, so an islet can surface at any ambient depth
instead of only where the seafloor happens to be shallow. After the curve's
toe compresses them they stand 5-8 m above the water: low, sandy, ringed by
beach -- which is what the existing biome rules make of that height, with no
biome-rule change.

THE THREE CONSTRAINTS, each by construction rather than by hope:

  Never merges with the mainland. The raise is EXACTLY zero wherever the
  ambient water is shallower than 14 m, so any continuous path from the
  mainland shore to an islet must cross that depth contour, and every pixel
  on it is untouched water. Measured: 0 of 13 islets merged, closest
  approach 78 px.

  Never touches the Trench. A distance mask zeroes the layer by 0.86 of
  half-map, and the ramp starts at 0.90. Measured max islet reach 0.795.
  All four corners stay below sea.

  Never spawns inland. Depth alone is not a test -- a deep lake and the
  carved crater bay are also below sea. The pre-Trench falloff is the honest
  discriminator (mainland coast sits near f = 0.66; islets need f > 0.72),
  and it is axis-invariant, because elongation moves WHERE a given f occurs,
  not the f at which land ends.

Two bugs found and fixed by measuring instead of trusting the first run:

  1. The density dial treated [-1,1] as the noise's actual range. Simplex
     is concentrated well inside it -- this field measures 0.050..0.958 --
     so "top 6%" resolved to a threshold almost nothing cleared: the first
     build raised 171 px on the entire map, none above sea. The threshold is
     now CALIBRATED against the field's own distribution (quantile over a
     1M-sample stride grid, deterministic from the seed), so the dial means
     what it says whatever FastNoiseLite returns.
  2. At 26/island the layer produced 77 islets under 200 px -- scatter, not
     "a handful". Retuned to 14/island at density 0.02: 13 islets of
     200-4792 px.

The pass prints its own islet area, so every run says what it did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 23:30:15 -04:00
566abe9784 feat: submarine coast shelf + elongation dials (terrain-water task 11)
COAST. The task's premise -- "the island edge is the steepest part" -- is
right, and the diagnostic locates it precisely: it is the SEABED, not the
land. HeightCurve.Apply returns early at and below sea level, so tasks
05-09 reshaped the land and never touched the water. Measured on the same
seed, distance-from-shore vs height:

                       land reaches 10 m   seabed reaches 10 m   gradient
  curve OFF                   31 px               31 px       0.254/0.258
  curve ON (ships)           186 px               31 px       0.038/0.258

So the landward side is ALREADY the broad shallow shelf terrain_design.md
asks for -- 54% of the island sits under 14 m -- and flattening it further
would only make it mushy. What is left is a shoreline that shelves gently
on land and then drops 6.7x steeper the moment it goes under.

CoastShelf fixes that side: depth' = depth * (1 - 0.775*exp(-depth/100 m)).
The seabed leaves the waterline at 22.5% of its former gradient and
recovers smoothly, so deep water and the Trench keep their shape. Measured
after: 5 m depth at 43 px (was 15), 10 m at 75 (was 31), 30 m at 150 (was
94) -- a 2.4-2.9x wider shallows.

It is strictly positive for positive depth, so it CANNOT move the waterline
by one pixel. Biomes and water bodies come out bit-identical, which is why
the coast is separable from elongation in the batch rather than tangled
with it -- contrary to the task's expectation that all coast work moves
biomes.

ELONGATION. IslandAxisX/Y replace the 1.15/0.90 literals (the spine shares
AxisX, as it always shared the literal). Sized by replaying candidate
falloffs against real terrain rather than guessing -- the replay reproduces
the shipped extents exactly, bbox and land count.

That replay says the task's suggested 1.30/0.85 buys +2.0% aspect, because
THE ISLAND IS ALREADY TRENCH-CLAMPED IN X: it spans 90.5% of the map width,
and 1.15 -> 1.40 moves the west coast only 393 -> ~360 px. Aspect responds
almost entirely to AxisY, which costs land:

  1.30/0.85  +2.0% aspect  +2.9% land      1.30/0.80  +4.9%  -2.2%
  1.30/0.82  +3.7% aspect  -0.1% land      1.30/0.75 +11.5%  -7.3%

Default 1.30/0.78 -- a visible step (+~8% aspect, measured 1.137 -> 1.200)
at ~4% land. The developer's eye tunes it from the batch; the table above
is the price list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 23:29:49 -04:00
896a428d47 fix: round the mountain-spine crest, the real centre line (terrain-water task 11)
The faint mid-map ridge is NOT the squircle/ellipse falloff blend that the
task suspected. Measured, read-only, before touching anything:

  bare falloff, slope step at centre   +2.05e-07  (SMALLER than the
    off-axis controls +2.36e-06 / +3.05e-06 -- Pow(2.5) crushes it,
    because the falloff is ~0 there)
  mountain spine, slope step at centre -7.58e-04  (170x the controls)

On real terrain, with the by-design Trench walls excluded, x = 4096..4099
hold the top FOUR slope-step locations out of 5999 columns. Column-mean
height runs 109.6 -> 135.5 -> 110.7 m across it: a symmetric ridge peaking
exactly on the centre column. Not the noise domain either -- the noise is
sampled over [0, MapSize], so coordinate zero is at the map CORNER. And not
seed-dependent: present on all 10 seeds checked, 6 positive AND 4 negative
(ranks 0, 18, 0, 83), so the "positive seeds only" impression does not hold.

Cause: the spine is Pow(1 - |x - cx|, 3), and |.| peaks at the axis with a
slope discontinuity of magnitude 2. SmoothAbs replaces |d| with
d^2/sqrt(d^2+e^2) -- zero with zero slope at the axis, converging to |d|
away from it -- so the crest rounds without dropping. e = 0.03 (~141 px)
cuts the 1-px kink by 99.3% (-7.64e-04 -> -5.41e-06, against a natural
profile curvature of ~1.1e-07), fills at most 3.9 m, and decays under
0.5 m by ~1100 px.

DEFERRED, reported not fixed: the spine's AXIS is still hard-coded dead
straight down x = centre for the full map height. That is what the
derivative render actually shows -- everything west of centre slopes east,
everything east slopes west. Rounding the crest does not make the range
meander. Per the task's "fix only if trivial, else report and defer" rule,
and confirmed with the developer, the axis is left alone: a wandering spine
is a world-shape change that rebaselines biomes on every seed, and it
deserves its own task and its own gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 23:28:45 -04:00
59f49ac8a2 fix: detail must yield to the crater carve (terrain-water task 10)
Batch 10_detail_edge measured 532 new below-sea pixels on seed
1158286446 and 44 on 72563200 -- exported terrain under the sea scalar
that the water grid (classify-driven, and correctly identical) calls dry.
Located: every one of them strictly inside the crater's physical carve
radius, 201-323 px out of 640.

Mechanism, confirmed against the data: detail moves a column's PRE-carve
height, and the carve is Lerp(curvedH, target, t). At t ~ 0.45-0.62 a
pre-carve drop of up to 8.4 m (relief plus the band-shift the warp
implies) still passes 0.02-3.53 m through, which is enough to push a
column sitting 0-3.45 m above the sea inside the bowl under it.

The crater carve is supposed to be the FINAL authority on its own
terrain, so detail now yields to it: CraterDetailWeight is 0 inside
0.80 x CraterRadius (exactly the carve's own radius) and feathers to
full by 1.05 x, scaling both the edge shift and the relief skin. Inside
the carve, B is bit-identical to A by construction, so the count is zero
rather than small. The reverted incision pass carried the same exclusion
for the same reason -- the principle outlived the pass that motivated it.

Re-running the 6 B_detail generations; A_off and the continuity run are
untouched by a detail-only change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:23:28 -04:00
af1a6d912b docs: TDTL v2 body, shelf-edge variation, erosion deferred (terrain-water task 10)
BLUEPRINT_FORMAT.md: TDTL rewritten for the v2 body (30 B) with the
version-skip rule spelled out -- this is the one section whose body is
VERSIONED rather than extended, because v1's layout was retired rather
than grown, and a reader that guesses would mint plausible nonsense.
Records edgeAmpM as APPLIED (post-clamp), so the file always describes
the terrain rather than the request. Config-knobs section updated:
ShelfEdgeVariation is metres of INPUT height -- a boundary displacement,
not an elevation.

Tools/Scripts/README.md: the two detail passes and why neither can breach
the red ceiling or the 420 m cap (K2/K6 do not move). Adds an explicit
"no rivers, no erosion, no flow routing anywhere in this pipeline" note
with the reverted D8 pass and its lesson on the record, so the next
reader does not re-derive it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:55:39 -04:00
ed16a7f668 tune: edge warp 5 m @12/island -> 12 m @20/island (terrain-water task 10)
Sized from measurement, not taste-first. |grad raw| at the K3/K4/K5
contours on seed 1375359975 is p50 0.00088 raw/px, so the first try (5 m,
12 undulations/island) displaced the shelf boundary a MEAN of 3.7 px with
a roughness ratio of 1.03 -- real, and invisible at map scale. At 12 m
and 20/island the boundary moves a mean 8.4 px (bench) / 9.1 px
(plateau), roughness ratio 1.09, and the outline grows the notches, coves
and peninsulas the sketch asked for.

Measured cost at 12 m (A off vs B, same seed): lowland and toe/red bands
bit-identical in both pixel count and slope -- the K2 pin holding exactly,
as designed. Foothill riser p50 0.681 -> 0.676 m/px, p90 1.445 -> 1.478,
max 3.09 -> 3.81. Bench p50 0.21 -> 0.232 (micro-relief included), still
buildable. Peaks max identical at 6.55.

Safety bound restated from "half the margin to a fixed knot" to 2/3 of
the smaller adjacent band, which is the constraint that actually matters:
the squeezed band never compresses below a third of its nominal width, so
its slope never more than triples. That puts the ceiling at 16.45 m for
the v5 knots and leaves the 12 m default real headroom if the gate says
"more".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:52:08 -04:00
492b56a87c feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10)
PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.

Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.

Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.

K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.

Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:31:41 -04:00
3b6d166458 revert: the D8 drainage incision, whole (terrain-water task 10)
The task-10 draft's PASS B shipped and produced the canonical grid
artifact: thousands of straight, disconnected, pooling scratches. Per-
cell steepest descent on a regular grid can only route along its eight
neighbour headings, so at map scale the "channels" read as hatching,
not drainage. Reverted entirely rather than tuned -- no K, p or mask
setting fixes a directional basis. Rivers and erosion move to Phase C
as a hydraulic-erosion pass over FINAL terrain.

Removed: FlowAccumulation / IncisionWeight / EdgeBlend and the INC_*,
SEA_CLAMP, SHELF_INC_WEIGHT and CRATER_* constants; RunIncisionPass and
the pass-2b call; the six incision fields from TDTL (writer, parser,
harness). KEPT, untouched and developer-approved: pass A, the shelf
micro-relief skin, and its ShelfReliefAmp dial.

The crater carve folds back into the single pass-2 loop it came from,
carving two LOCALS written once each. That is what the code did before
the draft split it out, and it makes the aliased double-carve that the
split introduced (fix 1b98fb5) structurally impossible rather than
merely fixed -- there is no longer a read-modify-write to get wrong.

TDTL bodies are now versioned (BlueprintFormat.TDTL_VERSION = 2) and the
parser skips a body version it does not know instead of misreading the
longer v1 layout into plausible nonsense. The only v1 payloads that
exist are in the reverted batch's own tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:30:44 -04:00
e3f7b508e7 chore: Godot .uid for TerrainDetailPass (terrain-water task 10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 20:01:42 -04:00
1b98fb5a17 fix: double crater carve on aliased maps when the curve is off (terrain-water task 10)
The pass-2c restructure carved sequentially through both map
references; with TerrainCurve off the classify map aliases the height
map, so the second Lerp re-carved already-carved cells — a deeper
bowl and shifted crater-adjacent water/biomes on curve-off runs only.
Caught by the task-10 continuity oracle (md5 vs the batch-08
curve-off baseline); curve-on runs were unaffected (separate arrays,
oracle 24/24). Both values are now read before either write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:52:33 -04:00
822c1530c3 tune: INC_K 0.78 -> 1.40 from the measured depth distribution (terrain-water task 10)
First tuning run (seed 1375359975): p50 2.3 / p90 6.1 / p99 13.6 / max
30.0 m — typical gullies under the 8-15 m target. K=1.40 confirmed:
p50 3.3 / p90 10.2 / p99 22.9 / max 30.0, zero sea-clamp hits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 18:20:16 -04:00
21dc3d16ce docs: TDTL section, TerrainDetail gate, v5 winner config notes (terrain-water task 10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 18:13:58 -04:00
45fe2ce034 feat: shelf micro-relief + drainage incision (terrain-water task 10)
Two paired detail passes on the locked v5 skeleton, output-heights
only (classify path untouched):

PASS A — shelf micro-relief: +-ShelfReliefAmp (default 3 m) Simplex
skin (seed resolved+7409, ~40 undulations/island ~ 200 m features),
weighted by shelf-ness (full mid-shelf, feathered to zero 30% of a
band half-width into the risers) — both shelves get their rolling
texture back; risers and peaks untouched.

PASS B — drainage incision: D8 steepest-descent routing + height-
ordered flow accumulation over the curved+relieved land
(TerrainDetailPass — pure array machinery, named C++ candidate per
D-035); depth = K*accum^p*slope (K=0.78, p=0.45 concave, cap 30 m),
masked full on risers / 30% on shelves / zero on the toe, above the
plateau top, and within 1.2x CraterRadius (feathered to 1.4x); hard
clamp: carved height >= sea + 1 m. Ordering: curve -> relief ->
incision -> crater carve (the carve stays the final authority; carve
moved to its own pass 2c, bit-identical expression on both maps).
The pass prints its own MEASURED depth distribution.

Gate TerrainDetail "off"|"v1" (default v1; both passes one judged
unit; no-op without the curve) + ShelfReliefAmp dial. New TDTL section
(38 B: version + relief/incision params + seed offset) across
writer/parser/harness — blueprints stay self-describing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 18:08:28 -04:00
c71026fd94 feat: task-09 verdict — BALANCED becomes plain "v5", COMPACT retires (terrain-water task 10 prerequisite)
The documented one-liner the task-09 gate triggers: TerrainCurve gate
is now "off"|"v5" (default v5 = the BALANCED preset, id 2);
"v5-compact" retired with a verdict-specific error, "v5-balanced"
redirects to plain "v5". Knots/TCRV encoding unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 18:00:21 -04:00
4797086a13 docs: curve v5 — TCRV preset extension, tri-state gate (terrain-water task 09)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 04:19:58 -04:00
02577872f2 feat: curve v5 — corner fixes + COMPACT/BALANCED taste presets (terrain-water task 09)
The three corner fixes (both presets): riser endpoint slope floors
0.2 -> 0.1 (0.1u + 0.9*smoothstep — climbs decelerate into shelves and
accelerate out); summit-spike base floor 0.1 -> 0.05
(0.05u + 0.95*u^4 — no hard skirt under the peaks); SHELF_SPAN_MIN
2 m -> 6 m (pronounced shelves keep a gentle tilt).

Knots are preset-parameterized (CurveKnots, explicit parameter — pure,
D-035), calibrated from the same pooled batch-04 CDF:
COMPACT  P65/77/84/88/94/97 -> 65/12/7/4/6/3/3 land split
BALANCED P60/73/83/88/96/99 -> 60/13/10/5/8/3/1
Gate: "off"|"v5-compact"|"v5-balanced" (batch-only tri-state;
interim default v5-balanced pending the developer's verdict — the
winner becomes plain "v5" in a follow-up commit); v1-v4 retired
loudly. Assertion runs per preset (8 corners x per-seed spikeMax; the
lowered floors stay strictly positive). TCRV v5 extension (+9 B):
preset id + K5/K6 — the effective curve is unambiguous from the
record; old records parse (version-dispatched); harness compares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 04:11:53 -04:00
68f7bca8e0 docs: curve v4 — TCRV modulation extension, TerrainCurve v4 default (terrain-water task 08)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:31:13 -04:00
4d2e024b2f feat: curve v4 — spatially modulated shelves (terrain-water task 08)
Per the task-07 gate: the terraces work but uniform anchors put a flat
ring at exactly 100 m and 220 m on every mountain. v4 keeps v3's
structure (frozen toe/rise, three risers, per-seed u4 spike to 420 m)
and turns the shelf anchors into smooth spatial fields: bench
100+-12 m and plateau 220+-20 m via two decorrelated very-low-freq
Simplex fields (~3 undulations per island width), plus a strength
field (~5/island) blending each shelf's output span between ~2 m
(pronounced flat) and ~25 m (barely a hint) — the optional strength
modulation shipped, ordering-safe by construction (worst-case bench
top 0.6962 < plateau min 0.9468). Field seeds derive from the RESOLVED
noise seed + fixed offsets (7101/7207/7303) — no config knob. Apply is
per-column with all modulated values as pure parameters (D-035); the
classify path never sees them.

Monotonicity assertion now sweeps all 8 modulation-extreme corners x
per-seed spikeMax. TerrainCurve gate "off"|"v4" (default v4);
v1-v3 retired loudly. TCRV extended (+36 B when version>=4:
amplitudes, span range, frequencies, seed offsets — length-framed
section makes the layout change safe; parser dispatches on the curve
version byte; harness compares the extension).

Note: MapGenerator.cs also carries the developer's editor whitespace
normalization (spaces->tabs, git diff -w empty before this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:30:27 -04:00
a046486a58 docs: curve v3 — TCRV per-version anchor semantics, TerrainCurve v3 default (terrain-water task 07)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 02:27:30 -04:00
ae7431ac87 feat: curve v3 — terraced ascent, two benches, white plateau at 220 m (terrain-water task 07)
Per the task-06 ground-test verdict (floor and 420 m ceiling frozen;
v2's ascent read flat-then-wall — 87% of the vertical budget in the
last 4% of input): the ascent is rebuilt as a terraced climb. Knots
recalibrated from the same pooled batch-04 land CDF at
P59/72/82/87/95/98 (K=0.509179/0.604081/0.698485/0.767213/0.930304/
1.050720), pooled land fractions exactly 59/13/10/5/8/3/2 (orange/red/
foothill-riser/bench/mid-riser/plateau/spike). Segments: frozen toe
and rise (storm anchors), smoothstep foothill riser to a near-flat
100 m bench, smoothstep mid riser to the near-flat white plateau at
220 m (relocated from 50 m — mountain towns/snow belong there),
per-seed-normalized u4 summit spike to 420 m (both retained from v2),
linear tail. Per-seed effective-curve monotonicity assertion retained.

TerrainCurve gate: "off"|"v3" (default v3); v1/v2 retired with a
loud config error (old blueprints regenerable). TCRV: knot slots carry
K1..K4 (K5/K6 are version constants), PlateauLo/Hi carry the two bench
anchors; version byte selects the semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 02:26:36 -04:00
5720f05a5b docs: curve v2 — TCRV spikeMax semantics, TerrainCurve v2 default, v1 retired (terrain-water task 06)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:29:34 -04:00
ac67c27952 feat: curve v2 — per-seed spike normalization, u4 wall, 420 m ceiling (terrain-water task 06)
Per the task-05 hillshade-gate verdict: lowlands frozen (identity/toe/
rise/shoulder/plateau byte-unchanged); the spike now maps [t4,
hMaxSeed] — each seed's own raw pre-curve maximum, computed in a new
pass-1 over GenerateTopography — onto the peak band, so every island
reaches the ceiling (v1's pooled-max domain left mid-range seeds at
110–175 m). Spike stiffened to 0.1u + 0.9u^4; peak cap raised to
420 m above sea (1.82869). Degenerate near-flat guard: spike domain
floored at t4 + 0.01. The curve is now SEED-DEPENDENT: hMaxSeed is a
pure parameter (D-035), recorded in TCRV (field renamed
HMaxCal -> SpikeMax, same byte layout; v1 semantics = pooled max, v2 =
per-seed), and the monotonicity assertion runs per generation against
the effective curve. TerrainCurve gate: "off"|"v2" (default v2);
"v1" retired with a loud config error (task-05 blueprints are
regenerable). Classify path untouched — the biome/water oracle must
hold unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:21:25 -04:00
46a991f352 fix: dedupe float samples in the monotonicity assertion (terrain-water task 05)
The coarse and fine sampling loops both touched h=0.10 and the two
doubles round to the same float32, so the strict check compared the
identity value against itself and threw on every curve-on run. Only
strictly increasing float samples are compared now. The curve itself
was never non-monotonic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:36:43 -04:00
9ebb8296c9 docs: TCRV section entry + TerrainCurve config note + 0_height snapshot (terrain-water task 05)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:26:09 -04:00
e7de292b3d feat: 0_height hillshade snapshot + TCRV blueprint section + harness (terrain-water task 05)
0_height: hypsometric storm-ladder tint x Lambert NW hillshade over
the curved heights, captured after topography in both curve modes —
relief is finally visible. Renumbers nothing.

TCRV (50 B, emitted when the curve is on): curve version + input knots
+ output bands, so blueprints are self-describing about the map their
heights went through (H7 spirit). Parsed into
WorldBlueprint.TerrainCurve (null = raw legacy profile); server treats
it as metadata. Round-trip harness compares it when present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:24:33 -04:00
67868638be feat: height-redistribution curve, gated + biome-invariant (terrain-water task 05)
HeightCurve (pure static, D-035): calibrated monotonic piecewise map —
identity at/below sea 0.15; ease-out toe to the orange ceiling 0.206
(75% coverage, knot t1=P75=0.628736 from batch-04's pooled flat-sea
land CDF, 340.6M samples); linear rise to red 0.27 (t2=P90); smooth
shoulder to the 50m plateau shelf 0.3492 (t3=P93); near-flat plateau
step (t4=P96); accelerating spike to the 220m peak cap 1.0265; linear
tail past the calibrated max. Strict monotonicity asserted numerically
at startup, loud throw on violation.

Applied in GenerateTopography AFTER noise+falloff+Trench, BEFORE the
crater carve (carve cuts curved terrain; rim/bowl untouched by the
curve). Biome oracle mechanism: a retained uncurved classify heightmap
(alias of _heightMap when off, zero cost) feeds biome rules, both
flood fills, and the shared water predicates — classification is
curve-invariant by construction. Towns/roads/diagnostics/exported
heights use curved terrain; town positions may legitimately move.

Config gate TerrainCurve: "off"|"v1" (default v1), unknown values
rejected loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:22:53 -04:00
9e55324668 fix: clamp town slope-check sampling to map bounds (terrain-water task 05)
PlaceTownNodes read _heightMap[rx ± slopeRadius] unclamped; candidates
spawn at [20, MapSize-20] but slopeRadius is 40 at 8K, so border
candidates indexed out of bounds. Never fired only because border land
stayed underwater and failed the above-sea test first — the
redistribution curve's flat coastal toe arms exactly that path
(D-033 consequence (b), recorded in sweep 01 and task 04).
Behavior-preserving except where it would have crashed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:20:16 -04:00
e435843e9a docs: SeaLevelModel/SeaLevelValue config notes (terrain-water task 04)
Both sea-level modes documented (flat default 0.15 / legacy field
Lerp); noted the blueprint does not record which model produced it —
the WBTB levels are the observable. Batch runner lives in /tmp
(throwaway), not in-repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 06:25:19 -04:00
200adcaf5a feat: config-gated flat-scalar sea level (D-033, terrain-water task 04)
SeaLevelModel: "flat" (one scalar, SeaLevelValue) or "field" (the
legacy latitude Lerp 0.26..0.15). Only GetSeaLevel's body changes —
all nine call sites (crater carve target, both flood fills, biome
split, beach band, Capitol window, town filters, A* zero-point)
inherit the model automatically; no consumer re-tuning. Default:
flat 0.15, pinned in ServerConfig.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 06:20:48 -04:00
1f957b3430 docs: water sections (WBID/WBTB/WSRF), no-BSIN rationale, SkipRoads + water stage in READMEs (terrain-water task 03)
Byte-accurate entries for the three water sections (transitional
per-body level rule, provisional salinity, WSRF sentinel/quantization),
updated size math (~576 MiB at 8K with water), the on-the-record
rationale for NOT serializing basins, and README updates for the new
pipeline stage, the 0_water snapshot, and the SkipRoads toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:40:48 -04:00
3bf822dac6 feat: WBID/WBTB/WSRF blueprint sections + parser + harness extension (terrain-water task 03)
Three new v2 tagged sections carrying the water-bodies stage output:
WBID (MapSize^2 u16 body ids; 0 none, 1 ocean, 2..N lakes), WBTB
(count-prefixed 20-byte records: id, type, provisional salinity,
transitional flat surface level, pixel count, centroid), WSRF
(MapSize^2 u16 quantized levels; 0 = no-water sentinel, 1 + L*32768
encoding, ~7.7 mm world resolution). Written only when the blueprint
carries water; absent on legacy re-encodes. Parser registers all
three (length + type checks) into new WorldBlueprint members —
consumed by nothing at runtime. Round-trip harness now compares the
water sections whenever the source carries them; v1-source baseline
stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:31:50 -04:00
d296ef30c4 feat: shared water predicates + water-bodies stage + priority-flood diagnostics (terrain-water task 03)
IsWaterPixel/IsOceanPixel/IsLakePixel are the single per-pixel water
truth; AssignBiomesAndDraw's water branch now calls them (verbatim
rules — behavior-preserving, proven by the bit-identical-biomes oracle
at acceptance). New IdentifyWaterBodies stage between the masks and
biomes: ocean = body 1, lakes labeled 2..N with CalculateTrueOcean's
4-connectivity in deterministic scan order; one transitional surface
level per body (GetSeaLevel at the body centroid; ocean at map
centre). 0_water snapshot painted from the stage's own outputs.

Priority-flood pit-fill (heap + pit-queue variant) runs as validated
diagnostics only — serializes nothing, asserts filled>=original and
full-map non-ascending drainage (reverse BFS, no sampling), reports
closed-basin statistics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:29:51 -04:00
81f7030140 feat: SkipRoads iteration toggle (terrain-water task 03)
Config flag SkipRoads (default false): when on, GenerateRoadsAsync and
the 3_roads snapshot are skipped entirely; export proceeds with all
four road sections present but empty (both v2 and legacy v1 permit
zero counts); the console states loudly that the blueprint is a
road-less iteration artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:27:36 -04:00
fb14b0693b chore: Godot .uid files for the new task-02 scripts
Generated by the engine on first run of the project after the v2
envelope landed (terrain-water task 02).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 00:17:46 -04:00
a7e6c17422 docs: BLUEPRINT_FORMAT.md + README updates for the v2 container (terrain-water task 02)
Byte-accurate v2 contract (magic/version, tagged sections, registered
FourCC table, validation rules, re-encode sentinels, extension path)
plus the v1 legacy summary — v1 stays live per the safety-net plan.
READMEs updated additively: parser dispatch, dual-write outputs,
harness usage, server params cross-check, wire-format hazard rewording
(u8 on v2 path, range-checked; append-only rule unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:31:03 -04:00
8532d88771 feat: blueprint v2 reader dispatch + validation + params cross-check (terrain-water task 02)
Reader sniffs the first byte (0x49 raw-ISLA v2 vs 0x07 v1 string
prefix) and routes to the v2 tagged-section parser or the intact v1
path (v1 loads log a deprecation warning — fallback stays live).

v2 validation: magic/version gate (loud reject), MapSize sanity bound
[256, 32768] before allocation, every section length checked against
remaining file, section-consumed-exactly check, params-first and
no-duplicate-section rules, biome and town-tier ordinal range checks.
Unknown tags skip by length — the forward-compat property D-030 buys.

ServerChunkManager cross-checks embedded params against config
(seed + MapSize) and logs a prominent desync warning — canon H7's
silent failure becomes loud. ServerConfig.json pins the reference
world: WorldSeed 1409879727, 8K.

Round-trip oracle GREEN: reference v1 (537,054,156 B) -> v2
(335,727,522 B, -192 MB) -> parse -> semantic equality holds
(heights bitwise, biomes, 94 towns, 4/1/37/39 road paths
point-for-point). v1 parse 1.9s, v2 write 1.4s, v2 parse 1.6s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:27:42 -04:00
c305fd6565 feat: blueprint v2 envelope — writer, dual-write, round-trip harness (terrain-water task 02)
v2 tagged-section container (D-030): raw ISLA magic + u32 version gate,
[u32 tag][u64 length][payload] sections — params (resolved generation
inputs incl. impact centre), heights f32, biomes u8, towns with the
highway-node flag, four road tiers identified by tag not position.

- BlueprintFormat.cs: the single tag/constant registry.
- BlueprintWriter.WriteV2: blueprint-typed, callable outside a
  generation run; generator and harness are both just callers.
- ExportMapData dual-writes: v2 under the primary seed name, legacy v1
  beside it as _v1.dat (safety net; removal is a future task).
- MapDataParser: LoadMapDataFromPath entry point; v1 parse body
  extracted intact as LoadV1 (v2 reader lands in the next commit).
- RoundTripHarness scene: load v1 -> write v2 -> load v2 -> semantic
  equality, headless, seconds per cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:24:08 -04:00
ad541dfbd8 feat: timestamped generation progress + staged snapshots (biomes/towns/roads)
Two additions, both observability only — no generation, routing, weight,
terrain or mesher behaviour changes.

1. Every progress print is now prefixed with elapsed time since generation
   started, e.g. [+ 142.7s], including a line per pipeline stage and per county
   road. The per-stage split (setup / continental loop / branch / county) can
   now be read straight off the console. Two A* fix attempts have been reasoned
   from structure rather than measurement; this makes the measurement free.

2. The end-of-run PNG capture was extracted into a reusable stage capture that
   can be called at any pipeline boundary. Snapshots are now taken after
   biomes, after towns, and after roads. The draw proxy renders whatever exists
   at the time, so stages that have not run simply do not appear — and a future
   water stage will show up automatically once it populates its data.

   The existing capture path is reused unchanged: same offscreen SubViewport,
   same draw proxy, same frame-yield plus double GPU wait. Those waits are
   load-bearing, not superstition, and each staged capture honours them.

Snapshot filenames gain a stage suffix: Map_Seed_<seed>_1_biomes.png,
_2_towns.png, _3_roads.png. Nothing reads these but a human, verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:43:19 -04:00
e070bbc9da fix: restore comment marker on WorldSeed line (build blocker)
Uncommenting the random-seed line left its trailing prose 'if 0 also
randomizes' as code, which stopped the whole project compiling. Only the
comment marker is restored — the choice of random-vs-fixed seed is left as set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:43:19 -04:00
bb0b4fe4f2 Revert "perf: weight-aware A* heuristic + tamed road repulsion (route-preserving speed fix)"
This reverts commit 1f7613439a.
2026-08-05 22:26:36 -04:00
783ee8b016 docs: fix MATH_MARCHING_CUBES §4 polarity + dims
Task 17 corrected the density sign convention in §1-§2 but left §4's worked
example on the old inverted convention, so the doc contradicted itself. §4 now
matches: negative is underground, positive is air. Also corrects a stale
16x16x64 chunk reference to 24x24x256.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:32:51 -04:00
1f7613439a perf: weight-aware A* heuristic + tamed road repulsion (route-preserving speed fix)
A* was searching almost the whole island for every road. Godot's stock
estimate-to-goal is plain straight-line distance, which assumes every step
costs 1 — but a step through mountains costs up to 401 and a step near an
existing road cost 10,001. With the estimate that far below reality, A* cannot
rule anything out, so it degenerates toward Dijkstra.

Two changes, both aimed at that:

1. RoadPathGrid overrides the estimate to scale it by HEURISTIC_WEIGHT (3.0),
   i.e. weighted A*. Paths may be up to 3x costlier than the theoretical best
   in exchange for exploring far less. This does NOT push roads over mountains:
   a ridge costs hundreds of times more than going around, which a 3x bias
   nowhere near pays for.

2. ROAD_REPULSION_PENALTY replaces the hardcoded +10000 with 250. The old value
   made ground near a road effectively infinite, and it compounded — each road
   drawn made the next search slower. 250 still strongly discourages roads from
   running alongside each other.

Terrain weights are untouched: the mountain curve (1 + elevation^3 * 400),
beach and wasteland costs are exactly as before, so mountains remain expensive
and roads still avoid them. Routing, tiers, smoothing and grid resolution are
also unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:32:51 -04:00
42f04139a2 refactor: extract FindPath seam (no behavior change)
Every road path search — highway, branch, county — now goes through one
function instead of three direct GetPointPath calls. The function body is
exactly the call it replaced, so behaviour is unchanged.

This exists so the next commit's pathfinding change can be judged on its own:
if the generated map changes after this point, it was the heuristic, not a
refactor slip.

Routing is untouched: which towns connect, loop order, Prim's daisy-chain,
tier assignment and the abandon protocol all deal only in town positions and
never touch the grid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:31:04 -04:00
d72ffc0183 docs: bring all READMEs current with actual code (post-sweep-10 truth pass)
Every README was read first, checked against the code in its directory, then
rewritten to describe what the code actually does now.

Corrected throughout: map size is config-driven (8192 default) not a fixed
4096; chunks are 24x24x256 not 32x32; road carving is implemented for all four
tiers, not a 'next step'; Data/ and Resources/ are empty, not populated.

Also fixed MATH_MARCHING_CUBES.md, which documented the density sign convention
exactly backwards — it claimed positive was underground, where the code treats
positive as above the surface and counts a corner inside when density < iso.
Getting that backwards inverts every normal, a bug this project has hit before.

The root README now carries accurate run steps: F5 runs the map generator (not
the game), F6 on Scenes/Main.tscn runs the 3D world, and ChunkRadius is a load
radius that should be dropped to 4-8 while iterating.

Docs only. No code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 05:23:58 -04:00
f9ea2da3f4 roads: enable Rugged+Trail carving + per-tier character (D-022)
All four road tiers now carve into the 3D world. Rugged and Trail were
generated, exported and parsed all along, but nothing ever consumed them —
which is why county roads visible on the 2D map did not exist in 3D.

Tier identity is now carried into the carve via a RoadSegment struct, so each
tier gets its own width, shoulder and grade-smoothing from one tunable block in
Constants: highways widest and holding a grade, trails narrow and hugging the
land. Rugged and Trail surface as dirt rather than asphalt.

Because tiers now have different reach, nearest-by-distance was no longer a
sound way to pick the governing road — a nearby footpath could shadow a highway
whose shoulder still covered the column. The carve now considers only roads
whose shoulder actually reaches, and takes the closest of those.

The per-chunk cull pad is derived from the widest shoulder plus a margin, so
widening a road cannot silently truncate it at chunk edges.

Untouched: density field, Marching Cubes interpolation/welding, chunk dims,
the .dat contract, the 2D A* generation, and mesher normals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 03:53:32 -04:00
a827595d61 whoops fix: bilinear height sampling in road carve (audit C10/Q6.B)
HeightAtPixel read a single heightmap cell after truncating the coordinate with
(int). Road path points are fractional and spaced under a metre apart, so
consecutive points truncated into the same cell (flat), then tipped into the
next one (a full inter-cell jump) — a metre-scale staircase under F2's fix.

It now reads the four surrounding cells and blends them bilinearly using the
fractional part of the coordinate, so height varies continuously with position.
Edge clamping is preserved: at the map border the second cell clamps back onto
the first, making the blend a no-op rather than an out-of-range read.

Only the sampling helper changed. Signature and both callers are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 03:27:47 -04:00
0910338a2a F2: continuous road grade — along-segment elevation + joint smoothing (D-021)
Road elevation is now sampled at the point on the road segment nearest the
column being carved, instead of at the segment's midpoint. The old behaviour
gave every column near a segment that segment's single midpoint height, so each
stretch of road was one flat plank and consecutive planks stepped like a
staircase wherever the road crossed a gradient.

Elevation now varies continuously along the segment, and because neighbouring
segments share an end point the height matches exactly at the joins. A new
ROAD_GRADE_SMOOTHING constant dials between holding a straight grade
(cut-and-fill) and hugging the land, per D-021.

Untouched: density field, Marching Cubes, chunk dimensions, the .dat contract,
and the 2D A* road network itself. Only how existing paths are carved into 3D.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 02:20:50 -04:00
ea11258871 F1: soft-fade biome color blending (BLEND_BAND_METERS, geometry untouched)
Vertex colour is now chosen from the true, unrounded surface height and faded
between adjacent materials across a tunable band, instead of switching at one
rounded integer depth. This removes the horizontal contour banding.

Colour path only: density field, Marching Cubes interpolation/welding, chunk
dimensions, the .dat contract and all road logic are untouched. BlockID
classification is unchanged and still drives non-visual use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:51:06 -04:00
7f0f43b5fb baseline: working salvaged prototype on Godot 4.7.1 (pre-F1)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:45:41 -04:00