diff --git a/Core/Scripts/MATH_MARCHING_CUBES.md b/Core/Scripts/MATH_MARCHING_CUBES.md index f3bd14b..6f53ad4 100644 --- a/Core/Scripts/MATH_MARCHING_CUBES.md +++ b/Core/Scripts/MATH_MARCHING_CUBES.md @@ -45,16 +45,17 @@ Instead of writing 256 `if/else` statements, the algorithm uses a hardcoded **Lo ### 4. Linear Interpolation (Why it looks Smooth) If the algorithm just drew triangles exactly halfway between the inside and outside corners, the terrain would still look a bit jagged (like a low-poly PS1 game). -To make it perfectly smooth, we **Interpolate**: -* Corner A has a density of `1.0` (Very solid). -* Corner B has a density of `-0.1` (Just barely in the air). -* Because `-0.1` is much closer to our `0.0` Iso-Level than `1.0` is, the algorithm slides the triangle vertex much closer to Corner B. This creates gentle slopes and sharp cliffs dynamically. +To make it perfectly smooth, we **Interpolate**. Remember the sign convention from §1 — +negative is underground, positive is air: +* Corner A has a density of `-1.0` (deep underground, very solid). +* Corner B has a density of `0.1` (just barely up in the air). +* Because `0.1` is much closer to our `0.0` Iso-Level than `-1.0` is, the algorithm slides the triangle vertex much closer to Corner B. This creates gentle slopes and sharp cliffs dynamically. --- ## ⚙️ The Execution Loop (What our C# Script will do) -When the Server tells the Client to render a 16x16x64 chunk, the mesher does this: +When the Server tells the Client to render a 24x24x256 chunk, the mesher does this: 1. Loops through every X, Y, Z coordinate in the chunk. 2. Checks the 8 corners of the current voxel. 3. Creates an `8-bit integer` (a byte) based on which corners are solid (e.g., `00001111`). diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index 9cd2f17..54c3c73 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -625,7 +625,9 @@ public partial class MapGenerator : TextureRect GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world..."); _highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear(); - AStarGrid2D astar = new AStarGrid2D(); + // RoadPathGrid is a stock AStarGrid2D with a scaled-up distance estimate, so the + // search stops flood-filling the island for every road. See the class for why. + AStarGrid2D astar = new RoadPathGrid(); astar.Region = new Rect2I(0, 0, MapSize, MapSize); astar.CellSize = new Vector2I(1, 1); astar.DiagonalMode = AStarGrid2D.DiagonalModeEnum.OnlyIfNoObstacles; @@ -653,8 +655,8 @@ public partial class MapGenerator : TextureRect Vector2 startPos = highwayNodes[i].Position; Vector2 endPos = highwayNodes[(i + 1) % highwayNodes.Count].Position; - Vector2[] rawPath = astar.GetPointPath( - new Vector2I((int)startPos.X, (int)startPos.Y), + Vector2[] rawPath = FindPath(astar, + new Vector2I((int)startPos.X, (int)startPos.Y), new Vector2I((int)endPos.X, (int)endPos.Y) ); @@ -688,7 +690,7 @@ public partial class MapGenerator : TextureRect } } - Vector2[] bossPath = astar.GetPointPath( + Vector2[] bossPath = FindPath(astar, new Vector2I((int)snowBoss.Position.X, (int)snowBoss.Position.Y), bestPixel ); @@ -744,8 +746,8 @@ public partial class MapGenerator : TextureRect else { // Attempt the connection. (If it freezes here, we know exactly which town caused it!) - countyPath = astar.GetPointPath( - new Vector2I((int)bestUnconnected.Position.X, (int)bestUnconnected.Position.Y), + countyPath = FindPath(astar, + new Vector2I((int)bestUnconnected.Position.X, (int)bestUnconnected.Position.Y), new Vector2I((int)bestConnected.Position.X, (int)bestConnected.Position.Y) ); } @@ -769,6 +771,19 @@ public partial class MapGenerator : TextureRect GD.Print("[A*] Logistics Network Complete!"); } + /// + /// The one place a road path is actually searched for. + /// + /// Every road — highway, branch, county — comes through here, so this is the single + /// seam where pathfinding can be changed without touching any routing decision + /// (which towns connect, in what order, at which tier). Those all live above and + /// only ever deal in town positions. + /// + private Vector2[] FindPath(AStarGrid2D astar, Vector2I from, Vector2I to) + { + return astar.GetPointPath(from, to); + } + private float GetSeaLevel(float t) => Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f)); private Vector2I FindClosestPixel(Vector2 pos, HashSet set) { @@ -823,6 +838,23 @@ public partial class MapGenerator : TextureRect } } + /// + /// How much extra a step costs near an already-drawn road. This is what stops new + /// roads from lying on top of old ones — it is SEPARATION, not terrain avoidance, + /// so it is unrelated to the mountain/beach/wasteland weights. + /// + /// It used to be 10,000, which was far past "discourage" — a normal step costs 1, + /// so it made the ground near a road effectively infinite. That wrecked the search: + /// every later road had to weigh routes costing tens of thousands, which is exactly + /// the situation A* handles by giving up on being clever and searching everything. + /// Worse, it compounded — each road drawn made the next one slower. + /// + /// 250 still means "strongly prefer not to run alongside an existing road" (250x a + /// normal step) without dwarfing the search. Raise it if parallel roads crowd + /// together; lower it if roads take silly detours to avoid each other. + /// + private const float ROAD_REPULSION_PENALTY = 250f; + private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius) { int step = Mathf.Max(1, radius / 2); @@ -834,8 +866,7 @@ public partial class MapGenerator : TextureRect int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1); var cell = new Vector2I(nx, ny); if (!astar.IsPointSolid(cell)) { - // The true Iron Curtain - astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f); + astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + ROAD_REPULSION_PENALTY); } } } @@ -937,6 +968,44 @@ public partial class MapGenerator : TextureRect } // Place this at the very bottom of the file! +/// +/// The road pathfinder's grid, with one change from stock: a scaled-up estimate. +/// +/// WHY THIS EXISTS. A* explores outward from the start until the cheapest route it +/// has found is provably the best. It decides how far to keep looking by comparing +/// what a route has cost so far against an ESTIMATE of what remains. Godot's stock +/// estimate is plain straight-line distance — it assumes every step costs 1. +/// +/// Our steps don't cost 1. A step through mountains costs up to 401, and a step near +/// an existing road used to cost 10,001. So the estimate was wildly optimistic about +/// the remaining journey, and A* responded the only way it can: by refusing to rule +/// anything out and searching almost the whole island for every single road. +/// +/// Multiplying the estimate makes the search commit to a direction sooner. It's a +/// well-known trade ("weighted A*"): paths are allowed to be up to HEURISTIC_WEIGHT +/// times more expensive than the theoretical best, in exchange for exploring far less. +/// We want believable roads, not provably-optimal ones, so that's a good trade. +/// +/// It does NOT make roads climb mountains. Crossing a ridge costs hundreds of times +/// more than walking around it — a 3x bias nowhere near pays for that. +/// +public partial class RoadPathGrid : AStarGrid2D +{ + /// + /// How strongly the search commits toward the goal. + /// 1.0 = stock behaviour: guaranteed-shortest, explores enormously. + /// 3.0 = default: still routes sensibly around terrain, explores far less. + /// higher = faster still, but paths get progressively less considered. + /// Raise it if generation is still slow; lower it if roads start looking careless. + /// + public const float HEURISTIC_WEIGHT = 3.0f; + + public override float _EstimateCost(Vector2I fromId, Vector2I toId) + { + return fromId.DistanceTo(toId) * HEURISTIC_WEIGHT; + } +} + public partial class MapDrawProxy : Control { public MapGenerator Source;