diff --git a/Core/Scripts/Constants.cs b/Core/Scripts/Constants.cs index 30db259..642e0a6 100644 --- a/Core/Scripts/Constants.cs +++ b/Core/Scripts/Constants.cs @@ -23,5 +23,23 @@ namespace IslaApocalypse.Core // 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 diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs index c9b8f37..5170f0e 100644 --- a/Server/Scripts/ServerChunkManager.cs +++ b/Server/Scripts/ServerChunkManager.cs @@ -135,17 +135,36 @@ namespace IslaApocalypse.Server foreach (var seg in localRoadSegments) { - float dist = DistanceToLineSegment(currentPos, seg[0], seg[1]); + // '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) { - // Sample the heightmap precisely at the center of the road segment - // so the whole road stays at a uniform elevation, ignoring the sloped mountain under it. - Vector2 midPoint = (seg[0] + seg[1]) / 2.0f; - float roadRaw = _blueprint.HeightMap[(int)midPoint.X, (int)midPoint.Y]; - closestRoadElevation = Mathf.Clamp(roadRaw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f); + // 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)); + + closestRoadElevation = Mathf.Lerp(landElevation, rampElevation, Constants.ROAD_GRADE_SMOOTHING); } } } @@ -210,21 +229,49 @@ namespace IslaApocalypse.Server renderer.RenderChunk(newChunk); } + /// + /// Turns a map-pixel position into a world surface height, with bounds clamping. + /// Same mapping used everywhere else: raw 0-1 heightmap value scaled into the + /// usable vertical band of the chunk. + /// + private float HeightAtPixel(Vector2 pixel) + { + int px = Mathf.Clamp((int)pixel.X, 0, _blueprint.MapSize - 1); + int py = Mathf.Clamp((int)pixel.Y, 0, _blueprint.MapSize - 1); + + float raw = _blueprint.HeightMap[px, py]; + return Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f); + } + /// /// Calculates the shortest distance from a point to a line segment defined by v and w. /// private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w) + { + return DistanceToLineSegment(point, v, w, out _); + } + + /// + /// Same as above, but also reports WHERE along the segment the nearest point falls: + /// is 0 at v, 1 at w. The road carving needs this so it can read + /// the height at the spot next to us instead of at the segment's midpoint. + /// + private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w, out float t) { float l2 = v.DistanceSquaredTo(w); - if (l2 == 0) return point.DistanceTo(v); // v == w case + if (l2 == 0) // v == w case + { + t = 0f; + return point.DistanceTo(v); + } // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. + // We find projection of point p onto the line. // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - float t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2)); - + t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2)); + // Projection falls on the segment - Vector2 projection = v + t * (w - v); + Vector2 projection = v + t * (w - v); return point.DistanceTo(projection); }