using System;
namespace IslaApocalypse.Core
{
///
/// ⭐ THE SCALING DISCIPLINE, IN CODE. → `Design - Tooling - Scaling Discipline.md`, D-002.
///
/// ═══ THE RULE ═══
///
/// NOTHING IN THE GENERATOR USES A RAW PIXEL NUMBER.
/// Every distance, radius, threshold and noise frequency is a FRACTION OF MapSize,
/// or derived from ScaleFactor.
///
/// ═══ WHY IT IS A RULE AND NOT A PREFERENCE ═══
///
/// These failures are SILENT. A hardcoded pixel count does not throw — the generator still
/// runs and still produces a map, and the map is just subtly wrong in ways that are hard to
/// attribute. The prototype learned this the expensive way: moving to 1:1 scale made the world
/// four times wider in pixels while the noise settings stayed put, and the terrain became
/// "TV static" — the same features packed into a quarter of the space. The same transition
/// broke a long tail of checks like "look 10 pixels away to see if there's water", which
/// quietly started measuring a quarter of the distance they used to.
///
/// In the developer's own words: "These kind of numbers I'm getting tired of changing because
/// of map size. We need to make sure these scale!"
///
/// ═══ ⚠ ONE BASELINE, DELIBERATELY ═══
///
/// The prototype ran TWO reference scales side by side: /1024 for noise and map drawing,
/// /4096 for town counts and road repulsion. Neither was wrong, but a reader — or a resize —
/// had to know which applied where, and the design doc's standing instruction is to "pick
/// deliberately and say which". This repo picks: ONE baseline, 1024, for everything. If a
/// second is ever genuinely needed it gets its own named type and its own reason, not a
/// bare divisor at a call site.
///
/// ═══ ⚠⚠ THE TIGHTENING OVER THE REFERENCE — NORMALIZED ADDITIVE OFFSETS ═══
///
/// The reference decorrelated noise layers with offsets in RAW PIXELS — GetNoise2D(x + 1000, …),
/// GetNoise2D(x * 1.5f + 5000, …). Because frequency already carries a 1/MapSize normalization,
/// a pixel-space offset does NOT hold still across map sizes: 1000 px is 1.0 normalized units
/// at 4K but 0.5 at 8K, so a resize silently samples a DIFFERENT SLICE of the noise field. The
/// feature scale is preserved; the REALIZATION is not. The same seed at two map sizes gives two
/// different worlds, for no reason anyone wrote down. (Found by the chat1/00 reference
/// inventory, §4.2 — where it also corrected the vault's standing diagnosis that the detail
/// noises were unscaled. They are scaled; it is the OFFSETS that are not.)
///
/// So in this repo: DECORRELATION OFFSETS ARE DECLARED IN MAP WIDTHS, via
/// . Never write a bare pixel constant into a noise coordinate.
///
/// No noise is built this phase. The convention is established before the generator exists so
/// that nothing is ever added unscaled — which is the only time this rule is cheap to hold.
///
public readonly struct GenerationScale
{
///
/// The reference map width the scaling baseline is anchored to. The world's noise looked
/// right at 1024 columns; every larger map stretches its features back out to match.
///
public const int BaselineMapSize = 1024;
///
/// The map's side in columns. 1 column = 1 metre (D-002, 1:1 scale).
///
/// ⚠ A GENERATION PARAMETER. Never a constant, never baked in. The rewrite's default target
/// is 10k x 10k, configurable 8k–12k (D-056) — but that is a DEFAULT chosen by config, and
/// no code below this line may assume it.
///
public readonly int MapSize;
public GenerationScale(int mapSize)
{
if (mapSize < BaselineMapSize)
throw new ArgumentOutOfRangeException(nameof(mapSize), mapSize,
$"MapSize must be at least the {BaselineMapSize} baseline.");
MapSize = mapSize;
}
///
/// MapSize / 1024. Divide a baseline-tuned noise frequency by this to hold feature size
/// constant in METRES as the map grows. A mountain range is the same physical size at any
/// map profile — which is what makes the map-size setting safe to change at all.
///
public float ScaleFactor => MapSize / (float)BaselineMapSize;
///
/// A distance, from a fraction of the map. Use this instead of writing a pixel count.
/// Fraction(0.75f) is "three quarters of the way across", at every map size.
///
public float Fraction(float fractionOfMap) => fractionOfMap * MapSize;
///
/// The inverse: what fraction of the map a distance in columns represents. For turning a
/// measured pixel distance back into a scale-free constant before you commit it to code.
///
public float AsFraction(float distanceInColumns) => distanceInColumns / MapSize;
///
/// A noise frequency, from one tuned at the 1024 baseline.
/// NoiseFrequency(0.004f) reproduces the reference's base terrain frequency at any
/// map size. → the reference's 0.004f / scaleFactor, chat1/00 §2.1.
///
public float NoiseFrequency(float baselineFrequency) => baselineFrequency / ScaleFactor;
///
/// ⭐ A decorrelation offset for a noise coordinate, declared in MAP WIDTHS.
///
/// OffsetInMapWidths(0.25f) shifts the sample by a quarter of the map at EVERY map
/// size — so two noise layers stay exactly as decorrelated at 12K as they were at 8K, and
/// the same seed produces the same world shape at any size.
///
/// ⚠ This is the one place a decorrelation offset may come from. A bare pixel constant in a
/// noise coordinate is the bug described in this type's summary; there is no correct value
/// for one.
///
public float OffsetInMapWidths(float mapWidths) => mapWidths * MapSize;
public override string ToString() => $"GenerationScale(MapSize={MapSize}, ScaleFactor={ScaleFactor:F3})";
}
}