using Godot;
namespace IslaApocalypse.Core
{
public static class BiomePalette
{
///
/// Determines the exact Block ID for a specific voxel in the 3D world based on its depth and biome.
///
/// 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)
{
// 1. Air Check: If we are above the terrain, it's Air.
if (currentY > surfaceHeight) return BlockRegistry.AIR;
// 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;
int depthBelowSurface = surfaceHeight - currentY;
// 4. Subsurface Logic (Dirt layer vs Stone layer)
// The top 4 blocks are usually dirt/sand, everything below that is solid stone.
if (depthBelowSurface > 4) return BlockRegistry.STONE;
// 5. Surface & Shallow Subsurface Logic (Based on Biome)
switch (biome)
{
case Biome.Beach:
case Biome.Crater:
return BlockRegistry.SAND; // Craters and Beaches are deep sand
case Biome.Snow:
case Biome.Mountain:
if (depthBelowSurface == 0) return BlockRegistry.SNOW; // Only the top layer is snow
return BlockRegistry.STONE; // Mountains have shallow topsoil, mostly stone
case Biome.Wasteland:
return BlockRegistry.WASTELAND_DIRT;
case Biome.Jungle:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_JUNGLE;
return BlockRegistry.DIRT;
case Biome.Paradise:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_PARADISE;
return BlockRegistry.DIRT;
case Biome.Tropical:
if (depthBelowSurface == 0) return BlockRegistry.GRASS_TROPICAL;
return BlockRegistry.DIRT;
default:
return BlockRegistry.DIRT; // Fallback
}
}
// ===================================================================
// SOFT-FADE COLOURING (rendering only)
// ===================================================================
// GetVoxelID above is the authoritative, INTEGER classification. It
// still decides what block is actually stored in each voxel, and it is
// deliberately left exactly as it was.
//
// What follows answers the same question — "what material is here?" —
// but for the RENDERER, and it does it differently in two ways:
// 1. It measures depth as a real number from the true surface height,
// instead of counting whole blocks down from a rounded one.
// 2. Instead of returning one material, it returns the two materials
// either side of the nearest boundary plus how far between them we
// are, so the renderer can fade rather than snap.
// That is the whole fix for the horizontal colour banding.
// Thickness of the top "skin" layer. Mirrors GetVoxelID's rule that
// only the single topmost block gets grass/snow/asphalt.
private const float SKIN_DEPTH_METERS = 1.0f;
// Depth at which we are into solid stone. Mirrors "depthBelowSurface > 4".
private const float STONE_DEPTH_METERS = 4.0f;
///
/// Picks the two materials to fade between for a point sitting
/// metres under the real surface.
///
/// Metres over which one material becomes the next. 0 = hard switch.
/// 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,
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 shallowID = GetShallowMaterial(biome);
// Two boundaries exist: skin->shallow and shallow->stone. Work out
// which one we are closest to and fade across that one.
if (depth < (SKIN_DEPTH_METERS + STONE_DEPTH_METERS) * 0.5f)
{
idNear = skinID;
idFar = shallowID;
blend = Fade(depth, SKIN_DEPTH_METERS, blendBand);
}
else
{
idNear = shallowID;
idFar = BlockRegistry.STONE;
blend = Fade(depth, STONE_DEPTH_METERS, blendBand);
}
}
///
/// 0 before the boundary, 1 after it, smoothly eased across a band of
/// metres centred on the boundary.
///
private static float Fade(float depth, float boundary, float band)
{
// A band of zero means "behave exactly like the old hard switch".
if (band <= 0.0f) return depth < boundary ? 0.0f : 1.0f;
float t = Mathf.Clamp((depth - (boundary - band * 0.5f)) / band, 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t); // smoothstep — eases in and out
}
/// The topmost material: what you actually walk on.
private static byte GetSkinMaterial(Biome biome, bool isRoad)
{
if (isRoad) return BlockRegistry.ASPHALT;
switch (biome)
{
case Biome.Beach:
case Biome.Crater: return BlockRegistry.SAND;
case Biome.Snow:
case Biome.Mountain: return BlockRegistry.SNOW;
case Biome.Wasteland: return BlockRegistry.WASTELAND_DIRT;
case Biome.Jungle: return BlockRegistry.GRASS_JUNGLE;
case Biome.Paradise: return BlockRegistry.GRASS_PARADISE;
case Biome.Tropical: return BlockRegistry.GRASS_TROPICAL;
default: return BlockRegistry.DIRT;
}
}
/// The layer just underneath the skin, before stone takes over.
private static byte GetShallowMaterial(Biome biome)
{
switch (biome)
{
// Sand goes all the way down on beaches and in the crater.
case Biome.Beach:
case Biome.Crater: return BlockRegistry.SAND;
// Mountains have essentially no topsoil — straight to rock.
case Biome.Snow:
case Biome.Mountain: return BlockRegistry.STONE;
case Biome.Wasteland: return BlockRegistry.WASTELAND_DIRT;
default: return BlockRegistry.DIRT;
}
}
}
}