using Godot; using System.Collections.Generic; using IslaApocalypse.Core; namespace IslaApocalypse.Server { public partial class ServerChunkManager : Node { private WorldBlueprint _blueprint; private Dictionary _activeChunks = new Dictionary(); public int chunkSize = 24; public override void _Ready() { // 1. LOAD CONFIGURATION and the seed-based blueprint data from the MapDataParser! ConfigManager.LoadConfig(); chunkSize = ConfigManager.ChunkRadius; // Update chunk radius from config file _blueprint = MapDataParser.LoadMapData(ConfigManager.WorldSeed.ToString()); if (_blueprint != null) { GD.Print("[Server] Blueprint loaded. Locating Capitol City..."); // 2. Find the Capitol in the parsed data Vector2 capitolPos = new Vector2(2048, 2048); // Safe fallback center foreach (var town in _blueprint.Towns) { if (town.Tier == TownTier.Capitol) { capitolPos = town.Position; break; } } // Old original png map coords // Vector2 capitolPos = new Vector2(2405, 3296); // hardcoded for now since we know exactly where it is in this seed, which is the "paradise" biome hub GD.Print($"[Server] Capitol found at {capitolPos}. Generating chunks..."); // 3. Convert pixel coordinates to Chunk coordinates int capitolChunkX = (int)(capitolPos.X / Constants.CHUNK_SIZE_X); int capitolChunkZ = (int)(capitolPos.Y / Constants.CHUNK_SIZE_Z); int radius = ConfigManager.ChunkRadius; for (int x = capitolChunkX - radius; x < capitolChunkX + radius; x++) { for (int z = capitolChunkZ - radius; z < capitolChunkZ + radius; z++) { GenerateChunk(new Vector2I(x, z)); } } // 5. Teleport the Camera to look down at our creation! Camera3D cam = GetNodeOrNull("Camera3D"); if (cam != null) { // Put the camera 120 meters in the air above the Capitol cam.GlobalPosition = new Vector3(capitolPos.X, 120f, capitolPos.Y); // Point it straight down at the ground cam.LookAt(new Vector3(capitolPos.X, 0, capitolPos.Y)); } } } public void GenerateChunk(Vector2I chunkCoord) { if (_activeChunks.ContainsKey(chunkCoord)) return; ChunkData newChunk = new ChunkData(chunkCoord); 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(); // 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 }); } } } // --------------------------------------- for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++) { for (int z = 0; z <= Constants.CHUNK_SIZE_Z; z++) { int globalX = startX + x; int globalZ = startZ + z; if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0) continue; float GetExactSurface(int gX, int gZ, out bool isRoad) { isRoad = false; if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1; if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1; float raw = _blueprint.HeightMap[gX, gZ]; float baseHeight = Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f); // If no roads are in this chunk, skip the heavy math entirely! 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; foreach (var seg in localRoadSegments) { float dist = DistanceToLineSegment(currentPos, seg[0], seg[1]); 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); } } } // 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); } 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); // We discard the 'out' variable for the normals using an underscore '_' float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _); float surfaceFwd = GetExactSurface(globalX, globalZ + 1, out _); Biome columnBiome = _blueprint.BiomeMap[globalX, globalZ]; // Hand the renderer the TRUE surface height for this column, plus the // biome and road flag that go with it. The BlockIDs below still use the // 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; float hRight = surfaceRight - exactSurfaceY; float hFwd = surfaceFwd - exactSurfaceY; float slopeX = hRight / Constants.VOXEL_SCALE; float slopeZ = hFwd / Constants.VOXEL_SCALE; float len = Mathf.Sqrt(slopeX * slopeX + 1.0f + slopeZ * slopeZ); for (int y = 0; y <= Constants.CHUNK_HEIGHT; y++) { float verticalDist = y - exactSurfaceY; float density = verticalDist / len; newChunk.Densities[x, y, z] = density; int blockY = Mathf.RoundToInt(exactSurfaceY); // THE PAINT: Pass isMainRoad into the Biome Palette! newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, isMainRoad); } } // End of Z loop } // End of X loop _activeChunks.Add(chunkCoord, newChunk); var renderer = new IslaApocalypse.Client.ChunkRenderer(); AddChild(renderer); renderer.RenderChunk(newChunk); } /// /// 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) { float l2 = v.DistanceSquaredTo(w); if (l2 == 0) return point.DistanceTo(v); // v == w case // Consider the line extending the segment, parameterized as v + t (w - v). // 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)); // Projection falls on the segment Vector2 projection = v + t * (w - v); return point.DistanceTo(projection); } } }