Stands up the CODE_REPO the rewrite is written into (D-049, D-058). Godot 4.7.2 /
.NET 8 / Godot.NET.Sdk 4.7.2. Nothing is generated, meshed, or ported — this is the
shell and the two data contracts.
Four layers, each with its boundary stated in a directory README:
Core/ math + data only, and engine-free — depends on nothing above it
Server/ authoritative logic (empty this phase)
Client/ rendering (empty this phase)
Tools/ the offline generator — may NOT reference Client (Phase 1 fills it)
Contracts (compiling stubs, no algorithms):
- Column model (D-053): a 2D grid of columns, each a stack of (material, thickness)
runs, any material at any depth. Air is the TOP RUN — there is no air-vs-solid
height branch, and no surface-height accessor exists to reintroduce one. Water is
not a band; it stays an overlay.
- Material schemas (D-054): terrain and building as two append-only registries,
bridged by a recipe seam that is deliberately EMPTY. Identity is a registry key,
never a serialized ordinal. Mesh style is per-ORIGIN and is not a material field.
Rails, so Phase 1 inherits them rather than rediscovering them:
- GenerationScale: MapSize is a parameter, everything derives from it, nothing uses
a raw pixel number. Tightening over the reference — decorrelation offsets are
declared in map widths, not pixels (chat1/00 §4.2).
- ToolingPaths: config/blueprint/output paths all env-overridable, resolved in one
place, so a batch cannot touch the developer's live files.
- FileSafety: the permanent no-deletion rules as throws rather than sentences.
user:// isolation: project name and use_custom_user_dir both pin the runtime dir to
~/.local/share/islaApocalypse-v2/, away from the old prototype's preserved seeds and
batches. Tools/Scenes/UserDirProbe.tscn confirms it rather than assuming it.
119 lines
5.9 KiB
C#
119 lines
5.9 KiB
C#
using System;
|
||
|
||
namespace IslaApocalypse.Core
|
||
{
|
||
/// <summary>
|
||
/// ⭐ 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
|
||
/// <see cref="OffsetInMapWidths"/>. 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.
|
||
/// </summary>
|
||
public readonly struct GenerationScale
|
||
{
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public const int BaselineMapSize = 1024;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public float ScaleFactor => MapSize / (float)BaselineMapSize;
|
||
|
||
/// <summary>
|
||
/// A distance, from a fraction of the map. Use this instead of writing a pixel count.
|
||
/// <c>Fraction(0.75f)</c> is "three quarters of the way across", at every map size.
|
||
/// </summary>
|
||
public float Fraction(float fractionOfMap) => fractionOfMap * MapSize;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public float AsFraction(float distanceInColumns) => distanceInColumns / MapSize;
|
||
|
||
/// <summary>
|
||
/// A noise frequency, from one tuned at the 1024 baseline.
|
||
/// <c>NoiseFrequency(0.004f)</c> reproduces the reference's base terrain frequency at any
|
||
/// map size. → the reference's <c>0.004f / scaleFactor</c>, chat1/00 §2.1.
|
||
/// </summary>
|
||
public float NoiseFrequency(float baselineFrequency) => baselineFrequency / ScaleFactor;
|
||
|
||
/// <summary>
|
||
/// ⭐ A decorrelation offset for a noise coordinate, declared in MAP WIDTHS.
|
||
///
|
||
/// <c>OffsetInMapWidths(0.25f)</c> 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.
|
||
/// </summary>
|
||
public float OffsetInMapWidths(float mapWidths) => mapWidths * MapSize;
|
||
|
||
public override string ToString() => $"GenerationScale(MapSize={MapSize}, ScaleFactor={ScaleFactor:F3})";
|
||
}
|
||
}
|