From f9ea2da3f4f059e3030c8f8e9658bf567dd3efa0 Mon Sep 17 00:00:00 2001 From: beezm Date: Wed, 5 Aug 2026 03:53:32 -0400 Subject: [PATCH] roads: enable Rugged+Trail carving + per-tier character (D-022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four road tiers now carve into the 3D world. Rugged and Trail were generated, exported and parsed all along, but nothing ever consumed them — which is why county roads visible on the 2D map did not exist in 3D. Tier identity is now carried into the carve via a RoadSegment struct, so each tier gets its own width, shoulder and grade-smoothing from one tunable block in Constants: highways widest and holding a grade, trails narrow and hugging the land. Rugged and Trail surface as dirt rather than asphalt. Because tiers now have different reach, nearest-by-distance was no longer a sound way to pick the governing road — a nearby footpath could shadow a highway whose shoulder still covered the column. The carve now considers only roads whose shoulder actually reaches, and takes the closest of those. The per-chunk cull pad is derived from the widest shoulder plus a margin, so widening a road cannot silently truncate it at chunk edges. Untouched: density field, Marching Cubes interpolation/welding, chunk dims, the .dat contract, the 2D A* generation, and mesher normals. Co-Authored-By: Claude Opus 5 (1M context) --- Client/Scripts/ChunkRenderer.cs | 2 +- Core/Scripts/BiomePalette.cs | 18 +-- Core/Scripts/ChunkData.cs | 7 +- Core/Scripts/Constants.cs | 156 ++++++++++++++++------ Core/Scripts/Enums.cs | 11 +- Core/Scripts/MarchingCubes.cs | 6 +- Server/Scripts/ServerChunkManager.cs | 185 ++++++++++++++++----------- 7 files changed, 255 insertions(+), 130 deletions(-) diff --git a/Client/Scripts/ChunkRenderer.cs b/Client/Scripts/ChunkRenderer.cs index ea3558b..2a46655 100644 --- a/Client/Scripts/ChunkRenderer.cs +++ b/Client/Scripts/ChunkRenderer.cs @@ -12,7 +12,7 @@ namespace IslaApocalypse.Client // surface/biome/road data it needs to fade colours smoothly. Mesh = MarchingCubes.GenerateMesh( data.Densities, data.BlockIDs, - data.SurfaceHeights, data.ColumnBiomes, data.ColumnIsRoad, + data.SurfaceHeights, data.ColumnBiomes, data.ColumnRoadMaterial, Constants.ISO_LEVEL, Constants.VOXEL_SCALE); diff --git a/Core/Scripts/BiomePalette.cs b/Core/Scripts/BiomePalette.cs index d1fb8d9..27875bb 100644 --- a/Core/Scripts/BiomePalette.cs +++ b/Core/Scripts/BiomePalette.cs @@ -10,8 +10,8 @@ namespace IslaApocalypse.Core /// The 2D biome pixel from the MapData. /// The maximum height of the terrain at this X/Z coordinate. /// The Y coordinate of the specific voxel we are painting. - /// Is this coordinate part of the highway/branch road network? - public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, bool isRoad) + /// What this column's road is paved with, or 0 if it is not a road. + public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, byte roadMaterial) { // 1. Air Check: If we are above the terrain, it's Air. if (currentY > surfaceHeight) return BlockRegistry.AIR; @@ -19,8 +19,9 @@ namespace IslaApocalypse.Core // 2. Bedrock Check: The very bottom of the world is indestructible. if (currentY == 0) return BlockRegistry.BEDROCK; - // 3. Infrastructure Check: If this is a road, and we are exactly on the surface, paint asphalt. - if (isRoad && currentY == surfaceHeight) return BlockRegistry.ASPHALT; + // 3. Infrastructure Check: If this is a road, and we are exactly on the surface, + // paint whatever that road tier is surfaced with (asphalt, dirt, ...). + if (roadMaterial != BlockRegistry.AIR && currentY == surfaceHeight) return roadMaterial; int depthBelowSurface = surfaceHeight - currentY; @@ -91,13 +92,13 @@ namespace IslaApocalypse.Core /// The material on the shallower side of the boundary. /// The material on the deeper side. /// 0 = fully idNear, 1 = fully idFar. - public static void GetBlendedVoxelIDs(Biome biome, bool isRoad, float depthBelowSurface, + public static void GetBlendedVoxelIDs(Biome biome, byte roadMaterial, float depthBelowSurface, float blendBand, out byte idNear, out byte idFar, out float blend) { // Above the surface can happen by a hair on interpolated vertices; treat as surface. float depth = Mathf.Max(depthBelowSurface, 0.0f); - byte skinID = GetSkinMaterial(biome, isRoad); + byte skinID = GetSkinMaterial(biome, roadMaterial); byte shallowID = GetShallowMaterial(biome); // Two boundaries exist: skin->shallow and shallow->stone. Work out @@ -130,9 +131,10 @@ namespace IslaApocalypse.Core } /// The topmost material: what you actually walk on. - private static byte GetSkinMaterial(Biome biome, bool isRoad) + private static byte GetSkinMaterial(Biome biome, byte roadMaterial) { - if (isRoad) return BlockRegistry.ASPHALT; + // A road paves over whatever biome it crosses, with its own tier's surface. + if (roadMaterial != BlockRegistry.AIR) return roadMaterial; switch (biome) { diff --git a/Core/Scripts/ChunkData.cs b/Core/Scripts/ChunkData.cs index 4205eca..3c44e63 100644 --- a/Core/Scripts/ChunkData.cs +++ b/Core/Scripts/ChunkData.cs @@ -30,7 +30,12 @@ namespace IslaApocalypse.Core // or storage depends on them. public float[,] SurfaceHeights = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1]; public Biome[,] ColumnBiomes = new Biome[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1]; - public bool[,] ColumnIsRoad = new bool[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1]; + + // What this column's road surface is paved with — asphalt for highways, + // dirt for trails, and 0 (BlockRegistry.AIR) for "not a road at all". + // Carries the tier's material rather than a plain yes/no, so a footpath + // doesn't get painted like a motorway. + public byte[,] ColumnRoadMaterial = new byte[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1]; // For our future delta-save system public bool IsPlayerProtected = false; diff --git a/Core/Scripts/Constants.cs b/Core/Scripts/Constants.cs index 642e0a6..b3fb362 100644 --- a/Core/Scripts/Constants.cs +++ b/Core/Scripts/Constants.cs @@ -1,45 +1,119 @@ +using Godot; + namespace IslaApocalypse.Core { - public static class Constants - { - // Chunk Dimensions - public const int CHUNK_SIZE_X = 24; - public const int CHUNK_SIZE_Z = 24; - public const int CHUNK_HEIGHT = 256; // Our agreed-upon depth! - - // World Generation - public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes - public const float VOXEL_SCALE = 1.0f; // 1 Pixel = 1 Meter + public static class Constants + { + // Chunk Dimensions + public const int CHUNK_SIZE_X = 24; + public const int CHUNK_SIZE_Z = 24; + public const int CHUNK_HEIGHT = 256; // Our agreed-upon depth! + + // World Generation + public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes + public const float VOXEL_SCALE = 1.0f; // 1 Pixel = 1 Meter - // --- APPEARANCE TUNING --------------------------------------------- - // How many metres it takes for one surface material to fade into the - // next (grass -> dirt -> stone) on the rendered mesh. - // - // TUNE THIS BY EYE: bigger = softer, more gradual fade. - // 0.0 = hard switch, exactly like the old banded look - // 2.0 = default, a gentle two-metre blend - // 4.0+ = very soft, materials smear into each other - // - // Colour only. This does NOT affect terrain shape, collision, or what - // block is actually stored in a voxel. - public const float BLEND_BAND_METERS = 2.0f; + // --- APPEARANCE TUNING --------------------------------------------- + // How many metres it takes for one surface material to fade into the + // next (grass -> dirt -> stone) on the rendered mesh. + // + // TUNE THIS BY EYE: bigger = softer, more gradual fade. + // 0.0 = hard switch, exactly like the old banded look + // 2.0 = default, a gentle two-metre blend + // 4.0+ = very soft, materials smear into each other + // + // Colour only. This does NOT affect terrain shape, collision, or what + // block is actually stored in a voxel. + public const float BLEND_BAND_METERS = 2.0f; - // --- ROAD GRADE TUNING --------------------------------------------- - // How much a road "holds a grade" instead of following every bump in - // the ground underneath it. Roads exist to be DRIVEN on, so this is a - // driveability setting, not a cosmetic one. - // - // 0.0 = hugs the land: the roadbed follows the terrain exactly. - // Never steps, but inherits every lump the ground has. - // 1.0 = holds a grade (default): each stretch of road is a straight - // ramp between its two path points, cutting through small - // bumps and filling small dips. Smoothest to drive. - // - // Values in between mix the two. Raise toward 1.0 for a more - // engineered road, lower toward 0.0 for a more rustic one. - // - // NOTE: this affects road ELEVATION (terrain shape under the road), - // not colour. Tune it properly once vehicles exist to drive on it. - public const float ROAD_GRADE_SMOOTHING = 1.0f; - } -} \ No newline at end of file + // --- ROAD CHARACTER BY TIER (D-022) --------------------------------- + // A road is carved to match how BUILT it is. One gradient, four steps: + // a highway is engineered, a trail is a footpath worn into the ground. + // + // Three knobs per tier: + // RoadRadius - half-width of the flat carriageway, in metres. + // ShoulderRadius - how far out the carve eases back to natural + // ground. Must always be LARGER than RoadRadius. + // GradeSmoothing - 0.0 hugs the land (follows every bump) + // 1.0 holds a grade (straight ramp between path + // points, cutting bumps and filling dips). + // SurfaceMaterial- what the carriageway is paved with. + // + // TUNE THESE BY EYE. They are starting values chosen to make the + // hierarchy visible, not final ones. A trail being bumpy is CORRECT — + // nobody drives a truck down a footpath. + // + // NOTE: these affect road ELEVATION (terrain shape) and paint, not the + // terrain noise. Tune the highway properly once vehicles can drive it. + + public const float HIGHWAY_ROAD_RADIUS = 4.0f; // 8 m carriageway + public const float HIGHWAY_SHOULDER_RADIUS = 12.0f; + public const float HIGHWAY_GRADE_SMOOTHING = 1.0f; // fully engineered + + public const float BRANCH_ROAD_RADIUS = 3.0f; + public const float BRANCH_SHOULDER_RADIUS = 9.0f; + public const float BRANCH_GRADE_SMOOTHING = 0.7f; + + public const float RUGGED_ROAD_RADIUS = 2.0f; + public const float RUGGED_SHOULDER_RADIUS = 6.0f; + public const float RUGGED_GRADE_SMOOTHING = 0.4f; + + public const float TRAIL_ROAD_RADIUS = 1.5f; + public const float TRAIL_SHOULDER_RADIUS = 4.0f; + public const float TRAIL_GRADE_SMOOTHING = 0.1f; // barely built at all + + /// + /// The widest reach any road has. The per-chunk road cull pads by this + /// (plus a margin) so a road just outside a chunk still carves into it. + /// Computed from the values above, so raising a shoulder can never + /// silently outgrow the cull and truncate roads at chunk edges. + /// + public static readonly float MAX_SHOULDER_RADIUS = Mathf.Max( + Mathf.Max(HIGHWAY_SHOULDER_RADIUS, BRANCH_SHOULDER_RADIUS), + Mathf.Max(RUGGED_SHOULDER_RADIUS, TRAIL_SHOULDER_RADIUS)); + + /// Safety margin added to the road cull, in metres. + public const float ROAD_CULL_MARGIN = 3.0f; + + /// + /// Looks up the carve settings for a road tier. One place to tune. + /// + public static RoadProfile GetRoadProfile(RoadTier tier) + { + switch (tier) + { + case RoadTier.Highway: + return new RoadProfile(HIGHWAY_ROAD_RADIUS, HIGHWAY_SHOULDER_RADIUS, + HIGHWAY_GRADE_SMOOTHING, BlockRegistry.ASPHALT); + case RoadTier.Branch: + return new RoadProfile(BRANCH_ROAD_RADIUS, BRANCH_SHOULDER_RADIUS, + BRANCH_GRADE_SMOOTHING, BlockRegistry.ASPHALT); + case RoadTier.Rugged: + return new RoadProfile(RUGGED_ROAD_RADIUS, RUGGED_SHOULDER_RADIUS, + RUGGED_GRADE_SMOOTHING, BlockRegistry.DIRT); + default: // Trail + return new RoadProfile(TRAIL_ROAD_RADIUS, TRAIL_SHOULDER_RADIUS, + TRAIL_GRADE_SMOOTHING, BlockRegistry.DIRT); + } + } + } + + /// + /// How one tier of road is carved. Values come from Constants above. + /// + public struct RoadProfile + { + public float RoadRadius; + public float ShoulderRadius; + public float GradeSmoothing; + public byte SurfaceMaterial; + + public RoadProfile(float roadRadius, float shoulderRadius, float gradeSmoothing, byte surfaceMaterial) + { + RoadRadius = roadRadius; + ShoulderRadius = shoulderRadius; + GradeSmoothing = gradeSmoothing; + SurfaceMaterial = surfaceMaterial; + } + } +} diff --git a/Core/Scripts/Enums.cs b/Core/Scripts/Enums.cs index f4e95fa..a550a5d 100644 --- a/Core/Scripts/Enums.cs +++ b/Core/Scripts/Enums.cs @@ -3,6 +3,13 @@ namespace IslaApocalypse.Core // The master list of shared identifiers for the game public enum Biome { Ocean, Lake, Beach, Paradise, Tropical, Jungle, Wasteland, Crater, Mountain, Snow } public enum TownTier { Village, Hub, Capitol, Outpost, IslandLoot, MiniPOI } - public enum MapHalf { Any, Left, Right } - + public enum MapHalf { Any, Left, Right } + + // How "built" a road is, most engineered first. Drives how wide and how + // smooth it gets carved into the 3D world (see D-022). + // + // NOTE: unlike Biome and TownTier above, this one is NOT written into the + // .dat file — the blueprint stores each tier in its own section, so this + // enum never becomes a serialized number. It is safe to reorder or extend. + public enum RoadTier { Highway, Branch, Rugged, Trail } } diff --git a/Core/Scripts/MarchingCubes.cs b/Core/Scripts/MarchingCubes.cs index 02ab820..5f11911 100644 --- a/Core/Scripts/MarchingCubes.cs +++ b/Core/Scripts/MarchingCubes.cs @@ -410,9 +410,9 @@ namespace IslaApocalypse.Core /// colour itself now comes from the per-column data below, which is not rounded. /// True (unrounded) surface height per X/Z column. /// Biome per X/Z column. - /// Whether each X/Z column is roadbed. + /// Road surface material per X/Z column, or 0 if not a road. public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs, - float[,] surfaceHeights, Biome[,] columnBiomes, bool[,] columnIsRoad, + float[,] surfaceHeights, Biome[,] columnBiomes, byte[,] columnRoadMaterial, float isolevel = 0.0f, float voxelScale = 1.0f) { var st = new SurfaceTool(); @@ -509,7 +509,7 @@ namespace IslaApocalypse.Core BiomePalette.GetBlendedVoxelIDs( columnBiomes[colX, colZ], - columnIsRoad[colX, colZ], + columnRoadMaterial[colX, colZ], depthBelowSurface, Constants.BLEND_BAND_METERS, out byte idNear, out byte idFar, out float blendAmount); diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs index 71a24f0..41faa67 100644 --- a/Server/Scripts/ServerChunkManager.cs +++ b/Server/Scripts/ServerChunkManager.cs @@ -4,6 +4,25 @@ using IslaApocalypse.Core; namespace IslaApocalypse.Server { + /// + /// One straight stretch of road, plus which tier it belongs to. The tier is what + /// decides how wide and how smoothly it gets carved (D-022) — before this existed, + /// every road was bulldozed to the same dimensions regardless of what it was. + /// + public struct RoadSegment + { + public Vector2 A; + public Vector2 B; + public RoadTier Tier; + + public RoadSegment(Vector2 a, Vector2 b, RoadTier tier) + { + A = a; + B = b; + Tier = tier; + } + } + public partial class ServerChunkManager : Node { private WorldBlueprint _blueprint; @@ -73,33 +92,40 @@ namespace IslaApocalypse.Server int startX = chunkCoord.X * Constants.CHUNK_SIZE_X; int startZ = chunkCoord.Y * Constants.CHUNK_SIZE_Z; -// --- NEW: SPATIAL CULLING FOR ROADS --- - // Create a bounding box for this chunk, plus a 15-meter padding to account for the road's dirt shoulders - Rect2 chunkBounds = new Rect2(startX - 15, startZ - 15, Constants.CHUNK_SIZE_X + 30, Constants.CHUNK_SIZE_Z + 30); - List localRoadSegments = new List(); +// --- SPATIAL CULLING FOR ROADS --- + // Bounding box for this chunk, padded so a road running just outside it still + // carves its shoulder in. The pad comes from the widest shoulder any tier has + // (plus a margin), so widening a road can't silently outgrow this and clip + // roads off at chunk edges. + float roadPad = Constants.MAX_SHOULDER_RADIUS + Constants.ROAD_CULL_MARGIN; + Rect2 chunkBounds = new Rect2( + startX - roadPad, startZ - roadPad, + Constants.CHUNK_SIZE_X + roadPad * 2, Constants.CHUNK_SIZE_Z + roadPad * 2); - // Filter Highways: Only save the segments that actually cross this specific chunk! - foreach (var highway in _blueprint.Highways) { - for (int i = 0; i < highway.Length - 1; i++) { - Vector2 a = highway[i]; - Vector2 b = highway[i+1]; - Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y)); - if (chunkBounds.Intersects(segBounds.Grow(1.0f))) { - localRoadSegments.Add(new Vector2[] { a, b }); - } - } - } - // Filter Branch Roads - foreach (var branch in _blueprint.BranchRoads) { - for (int i = 0; i < branch.Length - 1; i++) { - Vector2 a = branch[i]; - Vector2 b = branch[i+1]; - Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y)); - if (chunkBounds.Intersects(segBounds.Grow(1.0f))) { - localRoadSegments.Add(new Vector2[] { a, b }); + List localRoadSegments = new List(); + + // Keep only the segments that actually reach this chunk, and remember which + // TIER each came from — that is what decides how wide and smooth it carves. + void CollectSegments(List paths, RoadTier tier) + { + foreach (var path in paths) { + for (int i = 0; i < path.Length - 1; i++) { + Vector2 a = path[i]; + Vector2 b = path[i+1]; + Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y)); + if (chunkBounds.Intersects(segBounds.Grow(1.0f))) { + localRoadSegments.Add(new RoadSegment(a, b, tier)); + } } } } + + // All four tiers now carve. Rugged and Trail are the county roads — they were + // generated and saved all along, but nothing ever consumed them. + CollectSegments(_blueprint.Highways, RoadTier.Highway); + CollectSegments(_blueprint.BranchRoads, RoadTier.Branch); + CollectSegments(_blueprint.RuggedRoads, RoadTier.Rugged); + CollectSegments(_blueprint.TrailRoads, RoadTier.Trail); // --------------------------------------- for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++) @@ -112,9 +138,9 @@ namespace IslaApocalypse.Server if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0) continue; - float GetExactSurface(int gX, int gZ, out bool isRoad) + float GetExactSurface(int gX, int gZ, out byte roadMaterial) { - isRoad = false; + roadMaterial = BlockRegistry.AIR; // 0 == "not a road" if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1; if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1; @@ -125,67 +151,78 @@ namespace IslaApocalypse.Server if (localRoadSegments.Count == 0) return baseHeight; float finalHeight = baseHeight; - float minDist = 9999f; Vector2 currentPos = new Vector2(gX, gZ); - - float roadRadius = 4.0f; // The flat asphalt part (8 meters total width) - float shoulderRadius = 12.0f; // The sloped dirt/rock carving into the mountain - float closestRoadElevation = baseHeight; + // Details of the nearest road that actually REACHES us. Each tier has + // its own reach, so "nearest" alone isn't enough — a footpath two + // metres away must not shadow a highway whose shoulder still covers + // us. We therefore ignore any road we're outside the shoulder of, and + // take the closest of what's left. + bool foundRoad = false; + float bestDist = float.MaxValue; + float bestRoadElevation = baseHeight; + RoadProfile bestProfile = default; foreach (var seg in localRoadSegments) { + RoadProfile profile = Constants.GetRoadProfile(seg.Tier); + // 'alongT' tells us HOW FAR ALONG this segment the nearest point is // (0 = at the start point, 1 = at the end point). - float dist = DistanceToLineSegment(currentPos, seg[0], seg[1], out float alongT); - if (dist < minDist) - { - minDist = dist; - if (dist <= shoulderRadius) - { - // F2 FIX — the road elevation is now taken at the point on the - // segment CLOSEST TO US, not at the segment's midpoint. - // - // The old code gave every column near a segment that segment's - // single midpoint height, so each stretch of road was one flat - // plank and consecutive planks stepped up/down like a staircase. - // - // Two honest ways to read the height at our closest point: - // rampElevation - a straight line between this segment's two - // end points. Holds a grade; cuts and fills. - // landElevation - the actual terrain under that point. - // Hugs the land; inherits its bumps. - // ROAD_GRADE_SMOOTHING dials between them (see D-021). - // - // Either way it varies CONTINUOUSLY as we move along the road, - // which is what kills the steps. And because neighbouring - // segments share an end point, the height matches exactly where - // one segment hands over to the next — no seam at the joins. - float rampElevation = Mathf.Lerp(HeightAtPixel(seg[0]), HeightAtPixel(seg[1]), alongT); - float landElevation = HeightAtPixel(seg[0].Lerp(seg[1], alongT)); + float dist = DistanceToLineSegment(currentPos, seg.A, seg.B, out float alongT); - closestRoadElevation = Mathf.Lerp(landElevation, rampElevation, Constants.ROAD_GRADE_SMOOTHING); - } - } + if (dist > profile.ShoulderRadius) continue; // this road doesn't reach us + if (dist >= bestDist) continue; // a closer one already won + + // F2 FIX — the road elevation is taken at the point on the segment + // CLOSEST TO US, not at the segment's midpoint. + // + // The old code gave every column near a segment that segment's + // single midpoint height, so each stretch of road was one flat + // plank and consecutive planks stepped up/down like a staircase. + // + // Two honest ways to read the height at our closest point: + // rampElevation - a straight line between this segment's two + // end points. Holds a grade; cuts and fills. + // landElevation - the actual terrain under that point. + // Hugs the land; inherits its bumps. + // The tier's GradeSmoothing dials between them (D-021 / D-022): + // a highway holds its grade, a trail follows the ground. + // + // Either way it varies CONTINUOUSLY as we move along the road, + // which is what kills the steps. And because neighbouring + // segments share an end point, the height matches exactly where + // one segment hands over to the next — no seam at the joins. + float rampElevation = Mathf.Lerp(HeightAtPixel(seg.A), HeightAtPixel(seg.B), alongT); + float landElevation = HeightAtPixel(seg.A.Lerp(seg.B, alongT)); + + foundRoad = true; + bestDist = dist; + bestProfile = profile; + bestRoadElevation = Mathf.Lerp(landElevation, rampElevation, profile.GradeSmoothing); } - // THE BULLDOZER: Carve the terrain - if (minDist <= roadRadius) { - finalHeight = closestRoadElevation; // Flatten it completely! - isRoad = true; - } else if (minDist <= shoulderRadius) { - // Smoothly interpolate from the flat road up/down to the natural mountain height - float t = (minDist - roadRadius) / (shoulderRadius - roadRadius); - t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope - finalHeight = Mathf.Lerp(closestRoadElevation, baseHeight, t); + // THE BULLDOZER: Carve the terrain, using the winning road's own dimensions. + if (foundRoad) + { + if (bestDist <= bestProfile.RoadRadius) { + finalHeight = bestRoadElevation; // Flatten it completely! + roadMaterial = bestProfile.SurfaceMaterial; + } else { + // Smoothly interpolate from the flat road up/down to the natural mountain height + float t = (bestDist - bestProfile.RoadRadius) / (bestProfile.ShoulderRadius - bestProfile.RoadRadius); + t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope + finalHeight = Mathf.Lerp(bestRoadElevation, baseHeight, t); + } } return finalHeight; } - // Pass the boolean out for the main voxel so we can paint it! - bool isMainRoad = false; - float exactSurfaceY = GetExactSurface(globalX, globalZ, out isMainRoad); + // Pass the road surface out for the main voxel so we can paint it! + // 0 (AIR) means this column is not part of any road. + byte roadSurface = BlockRegistry.AIR; + float exactSurfaceY = GetExactSurface(globalX, globalZ, out roadSurface); // We discard the 'out' variable for the normals using an underscore '_' float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _); @@ -198,7 +235,7 @@ namespace IslaApocalypse.Server // rounded height (unchanged) — but the colour no longer has to. newChunk.SurfaceHeights[x, z] = exactSurfaceY; newChunk.ColumnBiomes[x, z] = columnBiome; - newChunk.ColumnIsRoad[x, z] = isMainRoad; + newChunk.ColumnRoadMaterial[x, z] = roadSurface; float hRight = surfaceRight - exactSurfaceY; float hFwd = surfaceFwd - exactSurfaceY; @@ -216,8 +253,8 @@ namespace IslaApocalypse.Server int blockY = Mathf.RoundToInt(exactSurfaceY); - // THE PAINT: Pass isMainRoad into the Biome Palette! - newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, isMainRoad); + // THE PAINT: Pass the road surface into the Biome Palette! + newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, roadSurface); } } // End of Z loop } // End of X loop