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 noise frequency stated as PERIODS PER MAP WIDTH — the reference's second frequency /// convention, used by every curve/detail modulation field. /// /// NoiseFrequencyPerMapWidth(40f) gives ~40 undulations across the island at any map /// size, which is exactly what the reference's periodsPerIsland / MapSize computed. /// /// ═══ ⚠ WHY THIS IS NOT A SECOND BASELINE ═══ /// /// It carries no baseline at all, so it does not reopen the /1024-vs-/4096 question this type /// exists to close. The two conventions answer different questions and both are scale-safe: /// /// "the frequency that looked right at 1024 columns" /// — a tuned number, normalized by ScaleFactor. /// "this many features across the island" /// — a stated intent, already size-independent. /// /// Reach for this one when the feature COUNT across the map is the thing being specified, and /// for when porting a frequency someone tuned by eye. /// public float NoiseFrequencyPerMapWidth(float periodsPerMapWidth) => periodsPerMapWidth / MapSize; /// /// ⭐ 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})"; } }