Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7085,6 +7085,51 @@ is **pre-approved** (keys in `tools/ai-assets/.env`, run via `uv`).

---

## ✅ Done (2026-08-03): terrain wonders — signature scars, continents, overhangs & the tunnel carver (#698–#709)

Three worldgen waves in one branch (⚠️ Waves 1+3 are a one-time reshape of existing worlds, accepted
like #576–#580; **continents are NEW WORLDS ONLY** via `WorldDescription.TerrainContinents`):

- **#698 mega-rift** — ~3 % of solid-air worlds carry ONE canyon girdling the whole planet (meandering
great-circle path, 100–200 deep, fjord-flooded floors, rim waterfalls for free).
- **#699 complex craters** — per-body rolls add central peaks (~45 %) + terraced walls (~40 %) to big
basins; hotspot **crater chains** (3–6 aligned bowls) + **ejecta rays** (deep-rock repaint, trig-free
dot-product cones).
- **#700 terrain grain** — per-world direction for dune seas + mountain chains via integer stretch +
period-normalised shear (⚠️ LatPeriod ≠ C/2 exactly — naive shear tears the seam; coefficients must
couple in units of the target axis's period).
- **#701 exotic accents** — hex basalt column fields (lava/ashen/basalt worlds), travertine spring
terraces with 1-deep deck pools (salt-white repaint), penitente blade-ice fields (cold worlds), salt
polygons (Voronoi ridge network on salt pans).
- **#702 ring calderas** (200–400 radius rim wall + sunken floor, self-flooding) + **whole-planet
escarpments** (~4 %: two storeys split by a meandering 60–100 cliff, applied pre-style).
- **#703 style×archetype hybrid** — a rolled 20–40 % of most styled worlds fades into the archetype
blend (deserts get gravel plains between dune seas); identity styles (flats/spires) stay pure.
- **#704 continents & real oceans** — NEW WORLDS ONLY: large planets (circ ≥ 8000, ~50 % roll, ocean
type excluded, lava/ashen included → basalt continents in lava oceans) get a domain-warped bimodal
platform/basin offset UNDER everything; the sea percentile targets the basin share so oceans settle
at the shelf. Plumbed: WorldDescription flag (default-true for new configs) → GameServer start →
`JoinAccepted.TerrainContinents` → client preview bakes (`WorldMinimap.Bake`). CLI escape:
`continents=off`.
- **#705 multi-band columns** — `GetExtraBands` generalises the floating-island second band: up to 6
bands/column (island tiers, ponds, caps, waterfalls); classic sky-island fill is bit-preserved.
- **#706 overhangs** — natural arches (abutment pillars + sagging rock bar), sea stacks (low-coast
pillars, ~60 % mushroom-capped), hoodoo fields (thin stems + wider caprocks) on badlands/tablelands/
mesa/canyons.
- **#707 verticality** — multi-tier skylands (1–3 layers, stalactite tapers, meadow ponds spilling
ENDLESS WATERFALLS over the rim), cenotes (30–80-deep sheer shafts with overhanging lips + turquoise
pools, caves open into the walls for free), underground mega-caverns (ellipsoid voids with
water/lava lakes + crystal-studded floors).
- **#708 tunnel carver** — deterministic hotspot worms (xorshift polylines, capsule y-spans per
column); **#709 consumers** — lava tubes (wider/smoother on volcano worlds), skylight shafts +
cave MOUTHS (tunnels may break the surface), waterfall plunge-pool incision (slot under falls),
crevasse fields on ≤ −8 °C worlds.
- **Tests** — `TerrainWondersTests` (21): seam wrap on wonder worlds, mega-rift wraps + depth,
escarpment storeys, caldera rim/floor, crater-trait distribution, chains, salt-ridge fraction,
continents gating (flag/size/ocean-identity) + bimodality + basin sea, band compat (tier-0 ==
classic query), cenote sheerness, tunnel determinism + surface mouths, chunk smoke on all wonder
worlds. Status: **local branch `terrain-wonders` only — NO PR yet** (user decision).

## ✅ Done (2026-08-03): beaches along seas and larger lakes (#679)

Coastlines ended abruptly — the biome surface (grass/mud/snow) ran straight into the water; sandy
Expand Down
5 changes: 5 additions & 0 deletions client/Assets/BlocksBeyondTheStars/Scripts/GameBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ public void SetLanguage(GameLocale locale)
/// <summary>The world seed (from JoinAccepted) — drives the per-world flora colours, also for
/// OTHER bodies seen from orbit (FloraTints is a pure function of seed + location + species).</summary>
public long WorldSeed => _worldSeed;

/// <summary>Whether this save was created with continents (#704, from JoinAccepted) — the local
/// preview generators (minimap/orbit bakes) must apply the same gate as the server.</summary>
public bool TerrainContinents { get; private set; }
private System.Collections.Generic.Dictionary<ushort, Color> _floraTintByBlock;

/// <summary>Total seconds this world has been played (from JoinAccepted, server-accumulated). The live
Expand Down Expand Up @@ -1214,6 +1218,7 @@ private void Start()
Debug.Log($"Joined as {m.PlayerId} at {LocationName} (seed {m.WorldSeed}).");
LoadingPlanetType = m.PlanetType;
_worldSeed = m.WorldSeed;
TerrainContinents = m.TerrainContinents; // #704: previews must match the server's gate
CumulativePlaytimeSeconds = m.CumulativePlaytimeSeconds; // saved world total; session ticks on top
if (_sessionStartRealtime < 0f)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ float Prominence(NetBody b)
if (planet != null)
{
baked = WorldMinimap.Bake(Game.Content, Game.Atlas, Game.WorldSeed, locationKey, body.PlanetType, bodyCirc, 96, 48,
bodyId: body.Id);
bodyId: body.Id, continents: Game.TerrainContinents);
tint = Color.Lerp(Color.white, sunHue, 0.35f); // light star-hue wash over the real map
}
else
Expand Down
6 changes: 4 additions & 2 deletions client/Assets/BlocksBeyondTheStars/Scripts/SpaceView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,7 +1357,8 @@ private void ShowLandMap(BlocksBeyondTheStars.Networking.Messages.NetLandingPad[
UiKit.Place(mapGo, pad, mapTop, mapW, mapH);
var raw = mapGo.AddComponent<UnityEngine.UI.RawImage>();
raw.texture = WorldMinimap.Bake(Game.Content, Game.Atlas, Game.WorldSeed, locName, typeKey, circ, 256, 128,
bodyId: !string.IsNullOrEmpty(_choosePadBody) ? _choosePadBody : Game?.LocationName ?? "home");
bodyId: !string.IsNullOrEmpty(_choosePadBody) ? _choosePadBody : Game?.LocationName ?? "home",
continents: Game?.TerrainContinents ?? false);
raw.raycastTarget = true;

// Day/night terminator: shade the night half of the strip + mark dawn/dusk, so you can see which pads
Expand Down Expand Up @@ -2834,7 +2835,8 @@ private void SpawnBody(string name, string bodyId, string kind, string locationN
int circ = WorldConstants.CircumferenceFor(
string.IsNullOrEmpty(bodyId) ? locationName ?? "home" : bodyId, ClassOf(kind, planetType), sizeBias);
var baked = WorldMinimap.Bake(Game.Content, Game.Atlas, Game.WorldSeed, locationName, planetType, circ, 96, 48,
bodyId: string.IsNullOrEmpty(bodyId) ? locationName ?? "home" : bodyId);
bodyId: string.IsNullOrEmpty(bodyId) ? locationName ?? "home" : bodyId,
continents: Game.TerrainContinents);
Color washTint = Color.Lerp(Color.white, sunHue, 0.35f);
sphere.GetComponent<Renderer>().sharedMaterial = LitPhase(washTint, sunDir, baked, new Vector2(1f, 1f));
}
Expand Down
5 changes: 3 additions & 2 deletions client/Assets/BlocksBeyondTheStars/Scripts/WorldMinimap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public static class WorldMinimap
/// world. <paramref name="locationName"/> stays the name-derived key the flora-tint rolls use.</param>
public static Texture2D Bake(GameContent content, BlockTextureAtlas atlas, long worldSeed,
string locationName, string planetTypeKey, int circumference, int texW, int texH,
string bodyId = null)
string bodyId = null, bool continents = false)
{
string key = $"{worldSeed}|{locationName}|{bodyId}|{planetTypeKey}|{circumference}|{texW}x{texH}";
string key = $"{worldSeed}|{locationName}|{bodyId}|{planetTypeKey}|{circumference}|{texW}x{texH}|{continents}";
if (_cache.TryGetValue(key, out var cached) && cached != null)
{
return cached;
Expand All @@ -56,6 +56,7 @@ public static Texture2D Bake(GameContent content, BlockTextureAtlas atlas, long
// Local preview generator: wrap size + the BODY identity (#478 — the body id salts the
// terrain seed, so the preview must carry it or it would show a different world's terrain).
gen.SetWorldMode(circumference, cratered: false, landingPads: null, bodyId);
gen.SetContinentsEnabled(continents); // #704: same creation-time gate as the server
int latPeriod = WorldConstants.LatitudePeriodFor(circumference);
int sea = gen.SeaLevel(planet);

Expand Down
35 changes: 35 additions & 0 deletions docs/developer/WORLD_GENERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,38 @@ When a chunk is generated (`WorldGenerator.Generate(planet, coord)`):

Every step is a pure function of `(seed, planetKey, coordinates)` — no persistent world storage, so
server and clients build the same chunk independently.

## 11. Terrain wonders (#698–#709, 2026-08-03)

Three stacked waves on top of §4's mechanisms — same determinism rules (pure functions of the seed,
hotspot-cell landmarks, quantile-calibrated consumers):

**Wave 1 — signature scars (class-B reshape).**
- *Mega-rift* (#698): ~3 % of solid-air worlds carry one meandering canyon girdling the entire planet
(`MegaRiftOffset` — a great-circle path periodic in X, not a hotspot cell).
- *Complex craters* (#699): per-body `CraterProfile` rolls central peaks + terraced walls; hotspot
crater chains + trig-free ejecta-ray repaint (`CraterChainCarve`, `CraterRayAt`).
- *Terrain grain* (#700): per-world direction for dunes/mountain chains via `GrainFbm` — integer
stretch + period-normalised shear. ⚠ The latitude period is NOT exactly circumference/2 (chunk
rounding), so shear must couple in units of the target axis's period or the seam tears.
- *Exotic accents* (#701): hex basalt column fields, travertine terraces (salt repaint + 1-deep deck
pools), penitente blade-ice fields, Voronoi salt polygons.
- *Ring calderas + whole-planet escarpments* (#702); *style×archetype hybrid* (#703): a rolled
20–40 % of most styled worlds fades into the archetype blend (identity styles flats/spires exempt).

**Continents (#704, NEW WORLDS ONLY).** `WorldDescription.TerrainContinents` (default true for new
configs, false on load) gates a bimodal platform/basin offset under everything on large planets
(circ ≥ 8000, ~50 % per-body roll, ocean type excluded, lava/ashen → basalt continents in lava
oceans). The sea percentile targets the basin share, so oceans settle at the shelf. The flag reaches
client preview bakes via `JoinAccepted.TerrainContinents`.

**Wave 2 — overhangs (#705–#707).** `GetExtraBands` generalises the floating-island band: up to 6
extra bands per column (island tiers/ponds/waterfalls, arch bars, sea-stack + hoodoo caps, cenote
lips), filled by `Generate`'s band switch. Multi-tier skylands (1–3 layers + stalactites, endless
rim waterfalls), cenotes (sheer shafts; caves open into the walls for free), underground
mega-caverns (`TryGetCavernSpan`, water/lava lakes, crystal-studded floors).

**Wave 3 — the tunnel carver (#708/#709).** `TunnelSpans` rebuilds a seeded worm polyline per
hotspot cell (xorshift, capsule y-spans per column) — noodle tunnels, wider lava tubes on volcano
worlds, skylight shafts, and real cave MOUTHS (tunnels may break the surface). River waterfall
columns incise a plunge-pool slot; ≤ −8 °C worlds grow crevasse fields.
5 changes: 4 additions & 1 deletion src/BlocksBeyondTheStars.GameServer/GameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,12 @@ public void Start()

_generator = new WorldGenerator(_meta.Seed, _content);
// World options: flora/ore factors are part of the save's description — set BEFORE any chunk
// generates so worldgen stays deterministic across reloads.
// generates so worldgen stays deterministic across reloads. Continents (#704) ride the same
// path: baked at creation, re-applied on every load, never flipped on an existing save.
_generator.SetWorldOptionFactors(
_meta.Description.FloraDensity.FloraFactor(),
_meta.Description.RareResources.OreFactor());
_generator.SetContinentsEnabled(_meta.Description.TerrainContinents);
_worlds = new WorldManager(_content, _generator, _repo);
BuildGalaxy(); // resolves _meta.ActiveLocationId to a concrete celestial body id
LoadPlayerStations(); // item 20 S4: restore persisted player stations onto the star map + registry
Expand Down Expand Up @@ -2422,6 +2424,7 @@ private void HandleJoin(int connectionId, JoinRequest join)
PlanetName = planetName,
SystemName = systemName,
CumulativePlaytimeSeconds = _meta.CumulativePlaytimeSeconds,
TerrainContinents = _meta.Description.TerrainContinents,
});
SendInventory(session);
SendPlayerState(session);
Expand Down
4 changes: 4 additions & 0 deletions src/BlocksBeyondTheStars.Networking/Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,10 @@ public sealed class JoinAccepted
/// <summary>Total seconds this world has been played (server-accumulated, see
/// <c>WorldMetadata.CumulativePlaytimeSeconds</c>); shown alongside the per-session timer in the HUD.</summary>
public long CumulativePlaytimeSeconds { get; set; }

/// <summary>Whether this save was created with continents (#704) — the client's preview generators
/// must apply the same gate or orbit/minimap textures would show the wrong coastlines.</summary>
public bool TerrainContinents { get; set; }
}

public sealed class JoinRejected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public sealed class ServerConfig
/// <see cref="BlocksBeyondTheStars.Shared.World.WorldDescription.SystemVariance"/> and
/// <see cref="BlocksBeyondTheStars.Shared.World.WorldDescription.AsteroidBelts"/> properties
/// default to false, so a loaded save whose metadata predates the features stays byte-identical.</summary>
public BlocksBeyondTheStars.Shared.World.WorldDescription World { get; set; } = new() { SystemVariance = true, AsteroidBelts = true };
public BlocksBeyondTheStars.Shared.World.WorldDescription World { get; set; } = new() { SystemVariance = true, AsteroidBelts = true, TerrainContinents = true };

/// <summary>Optional AI mission backend level (Off keeps the game fully AI-free).</summary>
public AiLevel AiLevel { get; set; } = AiLevel.Off;
Expand Down Expand Up @@ -595,6 +595,13 @@ public IReadOnlyList<string> ApplyCommandLine(string[]? args)
else if (string.Equals(value, "on", StringComparison.OrdinalIgnoreCase)) { World.AsteroidBelts = true; applied.Add("belts"); }
else if (string.Equals(value, "off", StringComparison.OrdinalIgnoreCase)) { World.AsteroidBelts = false; applied.Add("belts"); }
break;
case "continents":
// Continents & real oceans (#704). On for every new world by default; "off" restores
// the classic noise-coast terrain (same escape-hatch role as "variance"/"belts").
if (bool.TryParse(value, out var tc)) { World.TerrainContinents = tc; applied.Add("continents"); }
else if (string.Equals(value, "on", StringComparison.OrdinalIgnoreCase)) { World.TerrainContinents = true; applied.Add("continents"); }
else if (string.Equals(value, "off", StringComparison.OrdinalIgnoreCase)) { World.TerrainContinents = false; applied.Add("continents"); }
break;
case "danger":
// Global hostility multiplier (#547) — scales space-ambush odds + bandit-camp presence.
if (Enum.TryParse<BlocksBeyondTheStars.Shared.World.Frequency>(value, ignoreCase: true, out var dg)) { World.Danger = dg; applied.Add("danger"); }
Expand Down
9 changes: 9 additions & 0 deletions src/BlocksBeyondTheStars.Shared/World/WorldDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ public sealed class WorldDescription
/// (ServerConfig's default description carries true; a loaded save keeps whatever it stored).</summary>
public bool AsteroidBelts { get; set; }

/// <summary>Continents &amp; real oceans (#704): when true, large planets (circumference ≥ 8000) may
/// roll a bimodal continental-platform/ocean-basin terrain regime (~50 % of eligible bodies) — real
/// continents separated by real oceans, basalt continents in lava oceans on the volcanic types. MUST
/// default to false for the same reason as <see cref="SystemVariance"/>: terrain is re-derived from
/// the seed, and flipping this on an existing save would relocate the oceans wholesale — a player
/// base could wake up on the seabed. New worlds switch it on at creation (ServerConfig's default
/// description carries true; a loaded save keeps whatever it stored).</summary>
public bool TerrainContinents { get; set; }

// --- World options (creation-time; baked into the save's metadata — they shape worldgen) ---

/// <summary>Flora/tree density factor applied on top of each world's seeded variation.</summary>
Expand Down
Loading