islaApocalypse-v2/Tools/README.md
beezm fdf52f61ee Phase 1 review: raw grayscale, the wide gradient as hero, fixed batch naming
Presentation only. No generation file is touched — Topography, TerrainNoise,
IslandFalloff, Pass1Result, TerrainGenConfig and GenerationScale are all unchanged.

Batch naming, fixed in code. The prefix is the AUTHORING TASK number, not a running
counter: 04_review means "the batch task 04 authored", not "the fifth batch". It had
already drifted — tasks 02 and 03 produced 00_smoke through 05_trophy_10240 across
two tasks, so no folder name said which task made what. Now the task number is an
explicit argument to ToolingPaths.BatchRoot(taskNumber, descriptor), which composes
the prefix itself and REFUSES a descriptor that carries its own. All three tools
take it as ISLA_TASK. Verified: ISLA_BATCH=05_foo is refused with a stated reason
and exit 2, and creates no folder.

Also fixed: an exception out of _Ready does not stop Godot — it logs and the process
sits there with no main loop, so a misconfigured run HUNG rather than failing. A
hang looks like slow work, which is worse than a crash. The batch tools now catch,
print what was refused, and exit non-zero.

Grayscale mode: normalize a field to its own [min,max]. This is the one place
per-image normalization is correct — everywhere else anchors are fixed so images
compare, but here the point is to see one field at full contrast. The range is
printed and indexed so a shade reads back to a height. You cannot judge noise
through a palette: a ramp bends the distribution, a hillshade adds shape the data
does not have.

The wide Costa-Rica palette, and the reframing the developer asked for: the pretty
map is the WIDE GRADIENT RENDERED FLAT, and relief is no longer the hero. On raw
pass-1 noise a hillshade has nothing coherent to shade, so it renders fine fractal
bumpiness as fuzz that actively hides the elevation the colour is showing. Strong
hillshade is retained as diagnostic_relief and labelled a dev view — its bumpiness
is exaggerated slope, not extra terrain. Subtle relief is kept for comparison, with
the honest note that it still fuzzes until Phase 2 carves coherent landforms.

Legend: a colour bar with ticks drawn from the palette's own ramp, so it cannot
drift from the map beside it. Ticks are RELATIVE height and the image says NOT
METRES — the conversion is Phase 2's, and labelling it "m" would invent a fact in
the artefact a reader most trusts. Text comes from a 5x7 bitmap font written for
this, specifically so a legend does not drag in the SubViewport capture path.
2026-08-19 22:55:53 -04:00

281 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tools — the offline generator
**Holds:** the world generator and its diagnostics. Everything that produces a blueprint, and
nothing that consumes one at play time.
**Boundary — the tools wall:**
> ### ⚠⚠ TOOLS MAY NOT REFERENCE `Client/`. THE DEPENDENCY RUNS ONE WAY, OR THE WALL IS NOT A WALL.
>
> No UI, no player controllers, no shaders, no rendering code. `Tools/` may use `Core/`.
>
> **Two reasons, both from `Design - Tooling - Offline Generation.md`:** a lighter shipped client,
> and — less obviously — **not handing players the world-generation logic.** Shipping the generator
> lets it be reverse-engineered into an unfair map preview, with the whole island's layout, every
> settlement and every route known before anyone sets foot on the beach.
>
> **Tools emit data; they never write to a live save.** A generator that could touch live server
> state is a corruption risk for no benefit.
## What is here
### The generator — pass 1 (Phase 1)
**Ported from the reference's `MapGenerator.GenerateTopography` pass-1 loop** at tag
`pre-rewrite-reference` (`ab78883`). This is the crown-jewel port: **working code and its tuned
constants, carried over verbatim — not re-derived from a design summary** (→ D-050,
`Design - Rewrite - Extraction Manifest.md`).
| File | What it is |
|---|---|
| `Scripts/TerrainNoise.cs` | ⭐ The FastNoiseLite config, **every fractal property pinned explicitly** |
| `Scripts/Topography.cs` | ⭐⭐ Pass 1 — the six elements, in the reference's execution order |
| `Scripts/IslandFalloff.cs` | `SmoothAbs` + `CREST_EPSILON` (pass-1 half of the reference file) |
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
| `Scripts/HeightField.cs` | Raw `.f32` save/load — **the generation/presentation seam** |
| `Scripts/TerrainGenTool.cs` | Batch entry point (ladder + seed batch + `INDEX.md`) |
| `Scenes/TerrainGenTool.tscn` | Run this |
### The map renders — presentation (Phase 1, the look)
**Presentation only** (→ `Design - Rendering - Roughness Is Presentation.md`): it changes how height
is *shown*, never how it is *made*. It **cannot** change the terrain — it is handed a height field
loaded from a `.f32` dump and has no way to produce one.
| File | What it is |
|---|---|
| `Scripts/GrayscaleRenderer.cs` | ⭐ Plain grayscale — **for judging the noise itself** |
| `Scripts/ReliefPalette.cs` | The hypsometric palettes, stops placed on measured percentiles |
| `Scripts/ReliefRenderer.cs` | The tint (+ optional hillshade) blend |
| `Scripts/Hillshade.cs` | Horn 3×3 shaded relief; the `ZExaggeration` slope table |
| `Scripts/LegendRenderer.cs` | ⭐ The elevation legend strip |
| `Scripts/TinyFont.cs` | A 5×7 bitmap font, so a legend needs no viewport |
| `Scripts/LookConfig.cs` | The look dials and the named variants |
| `Scripts/ReviewBatchTool.cs` | ⭐ The review batch — grayscale + hero gradient + labelled relief |
| `Scripts/ReliefRenderTool.cs` | Taste-gate batch runner |
| `Scenes/ReviewBatchTool.tscn`, `Scenes/ReliefRenderTool.tscn` | Run these |
> ### ⭐ The pretty map is the WIDE GRADIENT, rendered FLAT. Relief is not the hero.
>
> A reference relief map gets its look from **wide colour + subtle relief + COHERENT TERRAIN.** We
> have the first. The third is Phase 2's curve and erosion. On raw pass-1 noise a hillshade has
> nothing coherent to shade, so it renders fine fractal bumpiness as **visual fuzz that actively
> hides the elevation the colour is showing.** So:
>
> | Look | Role |
> |---|---|
> | **`gradient_flat`** | ⭐ **the hero.** Wide Costa-Rica gradient, `HillshadeStrength = 0`, no relief at all |
> | `subtle_relief` | gentle relief (zex 18, strength 0.30) for comparison — **still fuzzes on raw noise** |
> | ⚠ `diagnostic_relief` | **A DEV VIEW, NEVER THE PRETTY MAP.** Strong exaggeration to make slope artifacts jump out. Its bumpiness is exaggerated *slope*, not extra terrain |
> | `atlas`, `dusk` | the task-03 plates, kept reachable |
>
> Relief comes into its own once erosion carves ridges and valleys worth lighting.
**Plain grayscale** (`GrayscaleRenderer`) normalizes a field to its own `[min,max]`. ⚠ This is the
one place per-image normalization is *correct* — everywhere else anchors are fixed so images compare.
Here the point is to see one field at full contrast, so the range used is printed and written into the
`INDEX.md`, and a shade reads back to a height. **You cannot judge noise through a palette:** a ramp
bends the value distribution and a hillshade adds shape the data does not have.
**The legend** (`LegendRenderer`) draws a colour bar with ticks from the palette's own ramp, so it
cannot drift from the map beside it. ⚠ **Ticks are RELATIVE height, not metres** — the metres
conversion is Phase 2's elevation profile, and labelling the bar "m" now would invent a fact. It says
so on the image. Text comes from `TinyFont`, a 5×7 bitmap font, specifically so a legend does not drag
in the SubViewport capture path (which awaits render frames and is why `--headless` hangs).
```bash
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/ReliefRenderTool.tscn
```
`ISLA_LOOKS=atlas,relief,dusk` · `ISLA_SOURCE` (batch to read `.f32` from) · `ISLA_MAPSIZE` ·
`ISLA_SEEDS` · `ISLA_BATCH` · `ISLA_DUMP_RAW=1`
**Three looks, deliberately** — a subjective gate drowns in a wall of near-duplicates, and each pair
isolates one question: `atlas` vs `relief` asks how strong the relief should be (same palette and
light); `atlas` vs `dusk` asks about palette and sun angle.
> ### ⚠ Vertical exaggeration is required, and it is a LOOK dial — not a physical claim.
>
> Height is in raw noise units over a 1 m grid, so the true per-pixel gradient is tiny: measured
> median land slope is **0.22°**. Un-exaggerated, the whole island shades as a flat plane. The raw
> units are **not metres** — the metres-per-unit conversion is Phase 2's, not this renderer's.
**The blend, and why it is not a multiply.** Hillshade on flat ground is `sin(altitude)` ≈ 0.71, so a
plain `tint × shade` darkens the entire map by 29% before any slope is involved and the hypsometric
tints are never actually seen. So the shade is normalized by its flat-ground value first — flat
terrain keeps its true tint, and only *slope* moves the colour — then shadows **multiply** while
highlights **screen** toward white. That asymmetry is the difference between a colour ramp and a map
you would frame.
**⚠ Relief fades out with depth below sea.** Not only taste: the deep floor is dominated by the
Trench, a synthetic additive wall whose gradient is ~1.5× the p99 *land* gradient. Shading it
faithfully draws a bright rim around the whole map and lights up the abyss with base-noise mottle —
relief on terrain nobody is meant to look at. The fade puts relief where the bathymetry is real (the
near-shore shelf) and lets the abyss lie flat, which is the cartographic convention anyway.
```bash
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn
```
`ISLA_MAPSIZE` (default 2048) · `ISLA_SEEDS` (comma-separated, positive) · `ISLA_BATCH` ·
`ISLA_SKIP_RAW=1` · `ISLA_LADDER=0`
**The six ported elements, in order** — the order is load-bearing, not an implementation detail:
1. **wobbled latitude scalar**`y/MapSize` plus a ±0.1 low-frequency wobble
2. **island falloff/mask** — squircle + ellipse, 50/50, then `Pow(·, 2.5f)`
3. **edge roughness**`noise(x·2.5, y·2.5)`, modulated by the squircle so it bites only at the rim
4. **southern sinker** — bottom 25%, **before** the power (its effect is superlinear)
5. **the Trench** — map-anchored, additive on both axes, **after** the power
6. **mountain spine**`SmoothAbs` crest, cubic, southern fade, amplitude `0.6f`
> ### ⚠ The latitude scalar is NOT climate temperature.
>
> The reference called it `temperature` and let biomes read the same array — a large part of how
> climate and terrain got fused. Here it is a **stage-1-local latitude field that terrain geometry
> reads**, and nothing else. Climate is **stage 3** and classifies finished shape (D-049 §2, D-056).
> Do not alias, store, or rename this into a climate map.
**Deferred to Phase 2, with the seam already open:** the submarine **coast shelf** and the
**offshore islets** (reference ~:621-664). Both act only below sea level and are judged once water
renders. `Pass1Result.PreTrenchFalloff` is captured at the exact point they consume, and
`Pass1Result.HMaxSeed` is carried for the seed-dependent redistribution curve.
**Not here at all:** the curve, erosion, rivers, water bodies, the crater, biomes, roads, the mesher.
### ⚠ The render writes a `Godot.Image` directly — not the capture path
The reference captured maps through a SubViewport + a `TextureRect._Draw`, because it composited
**vector overlays** (roads, town markers, river polylines) onto the raster. That machinery awaits
render frames — which is exactly why unattended runs need `xvfb-run` and why `--headless` hangs on
it.
**Phase 1 has no overlays.** A heightmap is a raster, so it is written per-pixel into an `Image` and
saved: no viewport, no frame awaits, no display. **Do not reintroduce the capture path by habit**
it becomes correct the moment something vector needs compositing, and not before.
### Diagnostics
- `Scenes/UserDirProbe.tscn` — confirms this project's `user://` is its own, not the old
prototype's. Exits non-zero on a clash.
- `Scenes/NoiseDefaultsProbe.tscn` — prints this engine's `FastNoiseLite` constructor defaults and
checks them against the recorded baseline. **Run it after any Godot upgrade.** It cannot change
the terrain (`TerrainNoise` pins every value); it makes a drifting default *visible* instead of
silent.
---
## ⭐ The conventions Phase 1's generator inherits
These were **earned, not assumed** — each one came out of the prototype's terrain arc.
`Design - Tooling - Iteration and Batching.md`.
### 1. `xvfb-run` for unattended generation — NOT `--headless`
> ### ⚠ `--headless` HANGS on a real generation run.
>
> The snapshot capture **awaits render frames, and there is no frame loop without a display.**
> **Measured, not assumed.** A run left on `--headless` does not fail loudly; it sits there.
```bash
xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 <scene>
```
A windowed run on the desktop also invites being closed mid-run by whoever is using the machine.
**The one exception is a job that awaits no frames** — like `UserDirProbe`, which is why that one
documents `--headless` explicitly:
```bash
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn
```
**If a job awaits a frame, it needs `xvfb-run`. When unsure, use `xvfb-run`** — it is correct in
both cases and costs nothing.
### 2. Environment overrides — safe BY CODE, not by care
Every path a tool reads or writes resolves through `Core/Scripts/ToolingPaths.cs`, and each has an
environment override, so a batch **cannot** read over or write to the developer's live files:
| Variable | Overrides | Default |
|---|---|---|
| `ISLA_CONFIG_PATH` | the generation config file | `user://config.json` |
| `ISLA_BLUEPRINT_PATH` | blueprints read/written | `user://blueprints` |
| `ISLA_OUTPUT_DIR` | generation output (batches live under it) | `user://output` |
> ⭐ **The point is that it is enforced by CODE, not by care.** A rule that depends on an executor
> remembering it will eventually meet an executor who does not.
There is no second way to obtain these paths. Do not add one.
### 3. ⚠ File safety — permanent rules
Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises.
1. **No deletion in the runtime root or anywhere under `batches/`.**
2. **Intermediates persist** in a `scratch/` subfolder that is **never cleaned.**
3. **The developer's placed files are never touched.**
4. **Any deletion is named explicitly in the run report.**
> ⚠ **These came from a real incident:** an executor admitted it had been deleting the developer's
> staged test blueprint. It was owned and fixed **in code.** That is why these are rules and not
> guidance.
### 4. Batch layout
```
batches/<task>_<descriptor>/<seed>_<variant>/
batches/<task>_<descriptor>/INDEX.md
batches/<task>_<descriptor>/scratch/ ← persistent; never cleaned
```
> ### ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER.
>
> `04_review` means *"the batch task 04 authored"*. It does **not** mean "the fifth batch". A task
> that produces six batches produces six `04_*` folders — not `04_` through `09_`.
>
> **This already drifted once.** Tasks 02 and 03 used the prefix as a global running counter and
> produced `00_smoke` … `05_trophy_10240` across two tasks, so nothing in a folder name said which
> task made what.
>
> **Enforced in code, not remembered:** the task number is an explicit argument to
> `ToolingPaths.BatchRoot(taskNumber, descriptor)`, which composes the prefix itself and **refuses a
> descriptor that carries its own** (`ISLA_BATCH=05_foo` → `REFUSED`, exit 2). Tools take it as
> `ISLA_TASK`.
**A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not
browsable.** The `INDEX.md` is what makes a batch readable a week later.
**Batches are OUTPUT.** They live under `ISLA_OUTPUT_DIR` (i.e. under `user://`), **not in this
repo.** The `batches/` folder here carries the convention, not the data. No batches are run this
phase.
### 5. Iteration levers, for when the generator exists
- **Skip the road pass for all terrain and water iteration.** In the prototype this cut a run from
~26 min to ~80 s — a **19× cut**, and the difference between iterating on terrain and not
iterating on terrain. Blueprints from such a run carry present-but-empty road sections, which is
**legitimate, not corrupt.**
- **⭐ Build the oracle before the taste-iteration, not after.** Because the classify path was
pinned to *uncurved* height, every shaping iteration had a hard automatic correctness check
(biome and water maps md5-identical). Five rounds of taste-iteration were safe **because
correctness was not being judged by eye.** *Where a future phase has a subjective gate, ask first
what the automatic invariant is.*
- **Config-gate every shaping change**, with the legacy behaviour surviving on the other side. It
keeps changes comparable as an A/B pair, and it means a rejected change costs a flipped default
rather than a reverted commit.
- **The lever is variants per batch, not speed per pass.** Two Phase B tasks ran ~2 hours each,
which looked like the pipeline getting slower. It was not — a single pass stayed at ~2 minutes.
The cost was A/B/ablation multiplication. **When a batch is genuinely wide, say so up front.**
### 6. Scaling discipline
**Nothing uses a raw pixel number.** Every distance, radius, threshold and noise frequency comes
from `Core/Scripts/GenerationScale.cs`. Read its header before adding any constant — including the
tightening on **normalized additive noise offsets**, which is a forward-guard for the Phase 1 noise
port. → `Design - Tooling - Scaling Discipline.md`.