Compare commits

..

No commits in common. "ad541dfbd8c00cd240c099748b6dea085421a81a" and "bb0b4fe4f29a6ea9384d517090eab8441ac8c4e4" have entirely different histories.

2 changed files with 23 additions and 75 deletions

View file

@ -7,8 +7,8 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
public static class ConfigManager public static class ConfigManager
{ {
// Default Fallbacks // Default Fallbacks
// public static int WorldSeed = 1063685222; public static int WorldSeed = 1063685222;
public static int WorldSeed = (int)GD.Randi(); // if 0 also randomizes // public static int WorldSeed = (int)GD.Randi(); if 0 also randomizes
public static int MapSize = 8192; public static int MapSize = 8192;
public static float CraterRadius = 400f; public static float CraterRadius = 400f;
public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount

View file

@ -17,10 +17,6 @@ public partial class MapGenerator : TextureRect
public bool IsHighwayNode; public bool IsHighwayNode;
} }
// When generation began, in engine milliseconds. Every progress print is stamped with
// the time since this, so the cost of each stage is readable straight off the console.
private ulong _pipelineStartMs;
private FastNoiseLite _noise; // The one and only original detailed noise! private FastNoiseLite _noise; // The one and only original detailed noise!
private Vector2 _impactCenter; private Vector2 _impactCenter;
private float _impactRadius; private float _impactRadius;
@ -39,8 +35,6 @@ public partial class MapGenerator : TextureRect
public override async void _Ready() public override async void _Ready()
{ {
_pipelineStartMs = Time.GetTicksMsec();
// 1. LOAD CONFIGURATION // 1. LOAD CONFIGURATION
ConfigManager.LoadConfig(); ConfigManager.LoadConfig();
MapSize = ConfigManager.MapSize; MapSize = ConfigManager.MapSize;
@ -72,71 +66,27 @@ public partial class MapGenerator : TextureRect
float randomY = (float)GD.RandRange(0.05f, 0.12f); float randomY = (float)GD.RandRange(0.05f, 0.12f);
_impactCenter = new Vector2(MapSize * randomX, MapSize * randomY); _impactCenter = new Vector2(MapSize * randomX, MapSize * randomY);
// --- THE GENERATION PIPELINE ---
// Each stage prints its elapsed time and drops a snapshot, so you can watch the
// world appear in layers instead of waiting for the whole run to judge any of it.
// A future water stage slots in as another CaptureStage call at its own boundary.
GenerateTopography(); GenerateTopography();
GD.Print($"{T()} Topography done (height + temperature).");
CalculateTrueOcean(); CalculateTrueOcean();
CalculateMainland(); CalculateMainland();
GD.Print($"{T()} Ocean and mainland masks done.");
AssignBiomesAndDraw(); AssignBiomesAndDraw();
GD.Print($"{T()} Biomes done.");
await CaptureStage("1_biomes");
GenerateTowns(); GenerateTowns();
GD.Print($"{T()} Towns placed: {_towns.Count}.");
await CaptureStage("2_towns");
// NEW: We await the roads so the engine doesn't freeze! // NEW: We await the roads so the engine doesn't freeze!
await GenerateRoadsAsync(); await GenerateRoadsAsync();
ExportMapData(); ExportMapData();
GD.Print($"{T()} Blueprint exported."); SaveMapSnapshot();
await CaptureStage("3_roads");
GD.Print($"{T()} GENERATION COMPLETE.");
} }
/// <summary> private async void SaveMapSnapshot()
/// Elapsed time since generation started, e.g. "[+12.4s]". Prefixed onto the progress
/// prints so the cost of each stage can be read straight off the console — no stopwatch.
/// </summary>
private string T()
{ {
double seconds = (Time.GetTicksMsec() - _pipelineStartMs) / 1000.0; // 1. THE FIX: Wait exactly one frame to let Godot finish _Ready() and unlock the Scene Tree!
return $"[+{seconds,6:F1}s]";
}
/// <summary>
/// Saves a PNG of the map AS IT STANDS RIGHT NOW, tagged with a stage label.
///
/// Call this at any pipeline boundary. It draws whatever exists at that moment — after
/// biomes you get bare terrain, after towns the town markers appear, after roads the
/// road network does. Nothing needs to be passed in or turned on; the draw proxy just
/// renders the lists as they currently are, so a future stage (water, for instance)
/// shows up automatically once it populates its data.
///
/// ⚠ DO NOT "SIMPLIFY" THE WAITS BELOW. They look like superstition and are not — each
/// one is there because removing it produced blank or half-drawn images. The design
/// vault records why (01_Design/tooling_notes.md). Symptoms of getting this wrong are
/// postage-stamp, black, or road-less captures.
/// </summary>
private async Task CaptureStage(string label)
{
// 1. Yield one frame. Godot locks the scene tree while it is processing, and we are
// about to add nodes to it. Without this, the AddChild below is unsafe.
await ToSignal(GetTree(), "process_frame"); await ToSignal(GetTree(), "process_frame");
GD.Print($"{T()} Capturing snapshot: {label}..."); GD.Print("Starting offscreen map capture...");
// 2. Build an invisible, offscreen "monitor" at the map's real size. It must be a // 2. Build the invisible, offscreen 4096 monitor
// SubViewport we size ourselves — rendering through the scene's UI layout makes
// Godot shrink the image to fit the physical screen.
var offscreenVP = new SubViewport(); var offscreenVP = new SubViewport();
offscreenVP.Size = new Vector2I(MapSize, MapSize); offscreenVP.Size = new Vector2I(MapSize, MapSize);
offscreenVP.RenderTargetUpdateMode = SubViewport.UpdateMode.Always; offscreenVP.RenderTargetUpdateMode = SubViewport.UpdateMode.Always;
@ -149,29 +99,27 @@ public partial class MapGenerator : TextureRect
bgRect.CustomMinimumSize = new Vector2(MapSize, MapSize); bgRect.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(bgRect); offscreenVP.AddChild(bgRect);
// 4. Attach the proxy to draw the roads and towns on top. Reading the viewport's // 4. Attach the proxy to draw the roads and towns on top
// texture alone would miss them — _Draw() output is not part of the base texture.
var drawProxy = new MapDrawProxy(); var drawProxy = new MapDrawProxy();
drawProxy.Source = this; drawProxy.Source = this;
drawProxy.CustomMinimumSize = new Vector2(MapSize, MapSize); drawProxy.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(drawProxy); offscreenVP.AddChild(drawProxy);
// 5. WAIT FOR THE GPU! Issuing draw calls is not the same as having drawn. // 5. WAIT FOR THE GPU! (Wait 2 render frames to guarantee the framebuffer is painted)
// Two frames — one proved insufficient in practice.
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
// 6. Pull the image from the invisible monitor and save it! // 6. Pull the image from the invisible monitor and save it!
Image capture = offscreenVP.GetTexture().GetImage(); Image capture = offscreenVP.GetTexture().GetImage();
string seedStr = _noise.Seed.ToString(); string seedStr = _noise.Seed.ToString();
string fileName = $"user://Map_Seed_{seedStr}_{label}.png"; string fileName = $"user://Map_Seed_{seedStr}.png";
Error saveResult = capture.SavePng(fileName); Error saveResult = capture.SavePng(fileName);
if (saveResult == Error.Ok) if (saveResult == Error.Ok)
GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}"); GD.Print($"Map saved successfully to: {ProjectSettings.GlobalizePath(fileName)}");
else else
GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}"); GD.PrintErr($"Failed to save map. Godot Error code: {saveResult}");
// 7. Delete the invisible monitor to free up RAM // 7. Delete the invisible monitor to free up RAM
offscreenVP.QueueFree(); offscreenVP.QueueFree();
@ -674,7 +622,7 @@ public partial class MapGenerator : TextureRect
// --- LOGISTICS & ROAD GENERATION --- // --- LOGISTICS & ROAD GENERATION ---
private async Task GenerateRoadsAsync() private async Task GenerateRoadsAsync()
{ {
GD.Print($"{T()} [A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world..."); GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
_highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear(); _highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear();
AStarGrid2D astar = new AStarGrid2D(); AStarGrid2D astar = new AStarGrid2D();
@ -697,7 +645,7 @@ public partial class MapGenerator : TextureRect
return angleA.CompareTo(angleB); return angleA.CompareTo(angleB);
}); });
GD.Print($"{T()} [A*] 1/4: Plotting the {highwayNodes.Count}-Node Continental Loop..."); GD.Print($"[A*] 1/4: Plotting the {highwayNodes.Count}-Node Continental Loop...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
if (highwayNodes.Count >= 3) { if (highwayNodes.Count >= 3) {
@ -720,7 +668,7 @@ public partial class MapGenerator : TextureRect
} }
// --- 2. THE MOUNTAIN BOSS BRANCH --- // --- 2. THE MOUNTAIN BOSS BRANCH ---
GD.Print($"{T()} [A*] 2/4: Connecting the Snow Boss..."); GD.Print("[A*] 2/4: Connecting the Snow Boss...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List<TownData> connectedTowns = new List<TownData>(highwayNodes); List<TownData> connectedTowns = new List<TownData>(highwayNodes);
@ -752,7 +700,7 @@ public partial class MapGenerator : TextureRect
} }
// --- 3 & 4. COUNTY ROADS (The Daisy-Chain Protocol) --- // --- 3 & 4. COUNTY ROADS (The Daisy-Chain Protocol) ---
GD.Print($"{T()} [A*] 3/4 & 4/4: Weaving County Roads (Daisy-Chaining)..."); GD.Print("[A*] 3/4 & 4/4: Weaving County Roads (Daisy-Chaining)...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List<TownData> unconnectedTowns = _towns.FindAll(t => !connectedTowns.Contains(t) && t.Tier != TownTier.IslandLoot); List<TownData> unconnectedTowns = _towns.FindAll(t => !connectedTowns.Contains(t) && t.Tier != TownTier.IslandLoot);
@ -783,7 +731,7 @@ public partial class MapGenerator : TextureRect
float actualDist = Mathf.Sqrt(shortestDist); float actualDist = Mathf.Sqrt(shortestDist);
// NEW: Live Diagnostics! Print the exact town we are testing BEFORE we calculate. // NEW: Live Diagnostics! Print the exact town we are testing BEFORE we calculate.
GD.Print($"{T()} -> ({count}/{totalToConnect}) Pathing {bestUnconnected.Tier} to grid. Dist: {actualDist:F1}px"); GD.Print($" -> ({count}/{totalToConnect}) Pathing {bestUnconnected.Tier} to grid. Dist: {actualDist:F1}px");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE! await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE!
Vector2[] countyPath = new Vector2[0]; Vector2[] countyPath = new Vector2[0];
@ -791,7 +739,7 @@ public partial class MapGenerator : TextureRect
// THE ABANDON PROTOCOL: If it's more than 8% of the map away, it's a hermit camp. Leave it! // THE ABANDON PROTOCOL: If it's more than 8% of the map away, it's a hermit camp. Leave it!
if (actualDist > (MapSize * 0.08f)) if (actualDist > (MapSize * 0.08f))
{ {
GD.Print($"{T()} [!] Too isolated. Abandoning road."); GD.Print($" [!] Too isolated. Abandoning road.");
} }
else else
{ {
@ -811,14 +759,14 @@ public partial class MapGenerator : TextureRect
} }
else if (actualDist <= (MapSize * 0.08f)) else if (actualDist <= (MapSize * 0.08f))
{ {
GD.Print($"{T()} [!] Path impossible (Trapped by terrain). Abandoning road."); GD.Print($" [!] Path impossible (Trapped by terrain). Abandoning road.");
} }
unconnectedTowns.Remove(bestUnconnected); unconnectedTowns.Remove(bestUnconnected);
connectedTowns.Add(bestUnconnected); connectedTowns.Add(bestUnconnected);
} }
GD.Print($"{T()} [A*] Logistics Network Complete!"); GD.Print("[A*] Logistics Network Complete!");
} }
/// <summary> /// <summary>