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