diff --git a/Client/Scripts/ChunkRenderer.cs b/Client/Scripts/ChunkRenderer.cs
index 2a46655..0988ebe 100644
--- a/Client/Scripts/ChunkRenderer.cs
+++ b/Client/Scripts/ChunkRenderer.cs
@@ -14,22 +14,149 @@ namespace IslaApocalypse.Client
data.Densities, data.BlockIDs,
data.SurfaceHeights, data.ColumnBiomes, data.ColumnRoadMaterial,
Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
-
+
StandardMaterial3D mat = new StandardMaterial3D();
-
+
// 2. THE FIX: Tell the material to use the colors we paint on the vertices!
- mat.VertexColorUseAsAlbedo = true;
+ mat.VertexColorUseAsAlbedo = true;
mat.Roughness = 0.8f;
- mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
-
+ mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
+
MaterialOverride = mat;
Position = new Vector3(
- data.ChunkPosition.X * Constants.CHUNK_SIZE_X * Constants.VOXEL_SCALE,
- 0,
+ data.ChunkPosition.X * Constants.CHUNK_SIZE_X * Constants.VOXEL_SCALE,
+ 0,
data.ChunkPosition.Y * Constants.CHUNK_SIZE_Z * Constants.VOXEL_SCALE
);
+
+ // 3. Water at rest (task 13) — its own mesh, its own material.
+ BuildWaterSurface(data);
+ }
+
+ ///
+ /// The water sheet, as a SEPARATE MeshInstance3D child.
+ ///
+ /// Why separate rather than a block in the terrain field: the terrain is a
+ /// Marching-Cubes iso-surface over ChunkData.Densities. Writing water into
+ /// that field would not lay a sheet on top of the seabed — it would move the
+ /// iso-surface, fusing the water into the terrain as if the sea were solid
+ /// ground. A second surface is the only way to have water sit AT its own
+ /// level, independent of the ground beneath it.
+ ///
+ /// It also makes translucency nearly free, which is why this task ships it
+ /// rather than deferring: a distinct MeshInstance3D carries its own
+ /// StandardMaterial3D, so alpha is one flag and Godot sorts transparent
+ /// surfaces after opaque ones on its own. No mesher change at all.
+ ///
+ /// Water at rest is FLAT — one level per cell, taken from the blueprint's own
+ /// body levels. No waves, no displacement, no animation (those are C2+).
+ ///
+ private void BuildWaterSurface(ChunkData data)
+ {
+ if (!data.HasAnyWater) return; // most inland chunks: nothing to build
+
+ var st = new SurfaceTool();
+ st.Begin(Mesh.PrimitiveType.Triangles);
+
+ int cellsX = Constants.CHUNK_SIZE_X;
+ int cellsZ = Constants.CHUNK_SIZE_Z;
+ bool any = false;
+
+ for (int x = 0; x < cellsX; x++)
+ {
+ for (int z = 0; z < cellsZ; z++)
+ {
+ // A cell is drawn if ANY of its four corners is wet, and it is drawn
+ // FLAT at the deepest-standing level among those wet corners.
+ //
+ // Drawing on a partly-dry cell is deliberate: it carries the sheet
+ // right up under the shoreline, where the opaque terrain in front of
+ // it hides the submerged part. The alternative — only fully-wet
+ // cells — retreats the waterline a metre from shore and leaves a
+ // visible dry gap all the way around every coast and lake.
+ float level = Constants.NO_WATER;
+ float d00 = data.WaterSurfaceY[x, z];
+ float d10 = data.WaterSurfaceY[x + 1, z];
+ float d01 = data.WaterSurfaceY[x, z + 1];
+ float d11 = data.WaterSurfaceY[x + 1, z + 1];
+ if (d00 > level) level = d00;
+ if (d10 > level) level = d10;
+ if (d01 > level) level = d01;
+ if (d11 > level) level = d11;
+ if (level <= Constants.NO_WATER) continue;
+
+ // Depth for colour: the deepest wet corner of the cell, so a shelving
+ // coast reads as a gradient rather than a staircase of flat tiles.
+ float depth = Mathf.Max(
+ Mathf.Max(data.WaterDepthM[x, z], data.WaterDepthM[x + 1, z]),
+ Mathf.Max(data.WaterDepthM[x, z + 1], data.WaterDepthM[x + 1, z + 1]));
+
+ Color c = DepthColor(depth);
+ float s = Constants.VOXEL_SCALE;
+
+ var p00 = new Vector3(x * s, level, z * s);
+ var p10 = new Vector3((x + 1) * s, level, z * s);
+ var p01 = new Vector3(x * s, level, (z + 1) * s);
+ var p11 = new Vector3((x + 1) * s, level, (z + 1) * s);
+
+ // Two triangles, wound so the surface faces up.
+ AddVert(st, c, p00); AddVert(st, c, p01); AddVert(st, c, p11);
+ AddVert(st, c, p00); AddVert(st, c, p11); AddVert(st, c, p10);
+ any = true;
+ }
+ }
+
+ if (!any) return;
+
+ var water = new MeshInstance3D();
+ water.Mesh = st.Commit();
+
+ var mat = new StandardMaterial3D();
+ mat.VertexColorUseAsAlbedo = true;
+ // Vertex alpha is only honoured when the material is actually transparent.
+ mat.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
+ // Lighting note: the scene's environment is minimal (one default
+ // DirectionalLight3D, a blue ambient), so a metallic surface reads almost
+ // black — metal shows its surroundings, and there is little to show. Water
+ // is therefore non-metallic with moderate roughness: albedo stays visible
+ // under weak ambient, and it still catches a highlight when a light does
+ // hit it. Left deliberately in the same lighting world as the terrain so
+ // the two improve together when the scene gets proper lighting.
+ mat.Roughness = 0.35f;
+ mat.Metallic = 0.0f;
+ // Both faces: the camera starts above, but a player will swim under it.
+ mat.CullMode = BaseMaterial3D.CullModeEnum.Disabled;
+ water.MaterialOverride = mat;
+
+ // Child of this chunk renderer, so it inherits the chunk's world offset
+ // and dies with the chunk.
+ AddChild(water);
+ water.Position = Vector3.Zero;
+ water.Name = "Water";
+ }
+
+ private static void AddVert(SurfaceTool st, Color c, Vector3 p)
+ {
+ // SetColor/SetNormal must be set immediately before AddVertex — same rule
+ // the terrain mesher follows.
+ st.SetColor(c);
+ st.SetNormal(Vector3.Up);
+ st.AddVertex(p);
+ }
+
+ ///
+ /// Shallow teal to deep navy, with alpha closing up as it deepens, so the
+ /// shelved coast stays readable and open ocean does not.
+ ///
+ private static Color DepthColor(float depthMeters)
+ {
+ float t = Mathf.Clamp(depthMeters / Constants.WATER_DEEP_METERS, 0f, 1f);
+ t = t * t * (3f - 2f * t); // smoothstep: hold the shallows, then fall away
+ Color c = Constants.WATER_SHALLOW.Lerp(Constants.WATER_DEEP, t);
+ c.A = Mathf.Lerp(Constants.WATER_ALPHA_SHALLOW, Constants.WATER_ALPHA_DEEP, t);
+ return c;
}
}
}
diff --git a/Core/Scripts/BlockRegistry.cs b/Core/Scripts/BlockRegistry.cs
index f435a9c..d4ec90a 100644
--- a/Core/Scripts/BlockRegistry.cs
+++ b/Core/Scripts/BlockRegistry.cs
@@ -21,6 +21,21 @@ namespace IslaApocalypse.Core
public const byte WASTELAND_DIRT = 9;
public const byte ASPHALT = 10; // For our roads!
+ // Water (terrain-water task 13). Wire-safe to add: block IDs are NEVER
+ // serialized — the blueprint's section table (BLUEPRINT_FORMAT.md) stores
+ // heights, biome ordinals and water data, never block IDs, and chunks are
+ // not persisted yet. So this ID is a runtime-only value and appending to
+ // the table cannot invalidate a .dat.
+ //
+ // NOTE this block is not placed into ChunkData.BlockIDs by the terrain
+ // path. Terrain is a Marching-Cubes iso-surface over a density field, and
+ // writing WATER into that field would FUSE the water into the terrain
+ // surface rather than lay a sheet on top of it. Water is its own mesh
+ // (ChunkRenderer). The entry exists so water has a real identity in the
+ // registry — a name and a colour — for the renderer and for whatever
+ // later needs to ask "what is this".
+ public const byte WATER = 11;
+
// This static constructor runs automatically the first time the registry is accessed
static BlockRegistry()
{
@@ -42,6 +57,10 @@ namespace IslaApocalypse.Core
// Infrastructure
Blocks.Add(ASPHALT, new BlockData(ASPHALT, "Asphalt", true, new Color(0.15f, 0.15f, 0.15f)));
+
+ // Water — not solid (you can move through it), shallow-water colour as the
+ // registry's representative value; the renderer shades by depth from there.
+ Blocks.Add(WATER, new BlockData(WATER, "Water", false, new Color(0.24f, 0.55f, 0.62f)));
}
public static BlockData GetBlock(byte id)
diff --git a/Core/Scripts/ChunkData.cs b/Core/Scripts/ChunkData.cs
index 3c44e63..9a60711 100644
--- a/Core/Scripts/ChunkData.cs
+++ b/Core/Scripts/ChunkData.cs
@@ -37,6 +37,23 @@ namespace IslaApocalypse.Core
// doesn't get painted like a motorway.
public byte[,] ColumnRoadMaterial = new byte[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+ // --- WATER AT REST (terrain-water task 13) ---------------------------
+ // Per column: the world-space Y of the water SURFACE, or Constants.NO_WATER
+ // where the blueprint says this column is dry. Filled by the server from
+ // the blueprint's WSRF/WBID/WBTB — the runtime never decides where water
+ // is, it only draws what the blueprint already classified.
+ //
+ // WaterDepthM is the TRUE depth in metres (water level minus seabed, both
+ // in blueprint units), kept separately because the rendered seabed is
+ // clamped at Y=2 and would flatten every deep-ocean column to the same
+ // value. Colour reads this; geometry reads WaterSurfaceY.
+ public float[,] WaterSurfaceY = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+ public float[,] WaterDepthM = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+
+ /// True if any column in this chunk carries water — lets the
+ /// renderer skip building a water mesh for the many inland chunks.
+ public bool HasAnyWater = false;
+
// For our future delta-save system
public bool IsPlayerProtected = false;
public bool NeedsSaving = false;
@@ -44,6 +61,12 @@ namespace IslaApocalypse.Core
public ChunkData(Vector2I position)
{
ChunkPosition = position;
+
+ // float[,] defaults to 0, which is a legal-looking water height. Start
+ // every column explicitly dry instead.
+ for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++)
+ for (int z = 0; z <= Constants.CHUNK_SIZE_Z; z++)
+ WaterSurfaceY[x, z] = Constants.NO_WATER;
}
}
}
diff --git a/Core/Scripts/Constants.cs b/Core/Scripts/Constants.cs
index b3fb362..0b2e689 100644
--- a/Core/Scripts/Constants.cs
+++ b/Core/Scripts/Constants.cs
@@ -75,6 +75,40 @@ namespace IslaApocalypse.Core
/// Safety margin added to the road cull, in metres.
public const float ROAD_CULL_MARGIN = 3.0f;
+ // --- WATER AT REST (terrain-water task 13) --------------------------
+ // The blueprint has carried water since task 03; this is only about
+ // DRAWING it. Water is a flat sheet at the body's own surface level —
+ // no waves, no flow, no animation. Those are C2+.
+
+ ///
+ /// Sentinel in ChunkData.WaterSurfaceY for "this column has no water".
+ /// Real surface heights are always >= 2 (the terrain clamp's floor), so a
+ /// negative value is unambiguous.
+ ///
+ public const float NO_WATER = -1.0f;
+
+ ///
+ /// Raw blueprint height -> world metres. One raw unit is this many metres,
+ /// and it is exactly the terrain mapping (CHUNK_HEIGHT - 5), named so the
+ /// water surface and the seabed cannot drift apart if the band is retuned.
+ ///
+ public const float HEIGHT_SCALE = CHUNK_HEIGHT - 5;
+
+ // Depth shading. Depth is measured from the BLUEPRINT heights, not the
+ // rendered geometry: the terrain render clamps its floor at Y=2, so deep
+ // ocean would otherwise all read as one flat ~36 m and the gradient would
+ // die exactly where the ocean gets interesting.
+ public const float WATER_DEEP_METERS = 60.0f; // depth at which the gradient bottoms out
+ public static readonly Color WATER_SHALLOW = new Color(0.38f, 0.76f, 0.78f);
+ public static readonly Color WATER_DEEP = new Color(0.05f, 0.17f, 0.42f);
+
+ ///
+ /// Water alpha, shallow -> deep. Shallows are clearer, so the shelved coast
+ /// and the seabed under it stay readable; depth closes the surface up.
+ ///
+ public const float WATER_ALPHA_SHALLOW = 0.62f;
+ public const float WATER_ALPHA_DEEP = 0.97f;
+
///
/// Looks up the carve settings for a road tier. One place to tune.
///
diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs
index ded094a..56f1f60 100644
--- a/Server/Scripts/ServerChunkManager.cs
+++ b/Server/Scripts/ServerChunkManager.cs
@@ -29,6 +29,11 @@ namespace IslaApocalypse.Server
private Dictionary _activeChunks = new Dictionary();
public int chunkSize = 24;
+ // Water-at-rest diagnostics (task 13), summed across the chunks generated.
+ private long _waterColumnsRendered = 0;
+ private int _chunksWithWater = 0;
+ private float _waterMinY = float.MaxValue, _waterMaxY = float.MinValue, _waterMaxDepth = 0f;
+
public override void _Ready()
{
@@ -69,7 +74,8 @@ namespace IslaApocalypse.Server
// 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
-
+ // New Manual Test point
+ // Vector2 capitolPos = new Vector2(4487, 4424);
GD.Print($"[Server] Capitol found at {capitolPos}. Generating chunks...");
@@ -87,6 +93,15 @@ namespace IslaApocalypse.Server
}
}
+ // Say what was actually drawn. The water is the blueprint's, not the
+ // runtime's, so if this reads zero the question is which of the two
+ // sides went quiet — and this line answers it without a debugger.
+ GD.Print($"[Server] Water at rest: {_waterColumnsRendered} water columns across " +
+ $"{_chunksWithWater} of {_activeChunks.Count} chunks" +
+ (_waterColumnsRendered > 0
+ ? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m."
+ : " — nothing to draw here (check the blueprint carries WBID/WSRF)."));
+
// 5. Teleport the Camera to look down at our creation!
Camera3D cam = GetNodeOrNull("Camera3D");
if (cam != null)
@@ -253,6 +268,46 @@ namespace IslaApocalypse.Server
newChunk.ColumnBiomes[x, z] = columnBiome;
newChunk.ColumnRoadMaterial[x, z] = roadSurface;
+ // --- WATER AT REST (task 13) ---------------------------------
+ // The blueprint is the AUTHORITY on where water is; the runtime
+ // only draws it. WBID decides presence (it is the water stage's
+ // own classification output, the same set the biome grid's
+ // Ocean/Lake pixels form by construction), WSRF gives the level
+ // per pixel, and the WBTB body table is the fallback if a column
+ // is flagged wet but carries the WSRF no-water sentinel.
+ if (_blueprint.WaterBodyIds != null)
+ {
+ ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ];
+ if (bodyId != 0)
+ {
+ float levelRaw = -1f;
+ if (_blueprint.WaterSurfaceQ != null)
+ {
+ ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ];
+ if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q);
+ }
+ if (levelRaw < 0f) levelRaw = BodyLevel(bodyId);
+
+ if (levelRaw >= 0f)
+ {
+ // Same mapping as the terrain, so the sheet and the
+ // seabed cannot drift apart.
+ newChunk.WaterSurfaceY[x, z] = Mathf.Clamp(
+ levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
+ // TRUE depth, from blueprint units — see ChunkData.
+ newChunk.WaterDepthM[x, z] =
+ Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE);
+ newChunk.HasAnyWater = true;
+
+ _waterColumnsRendered++;
+ float wy = newChunk.WaterSurfaceY[x, z];
+ if (wy < _waterMinY) _waterMinY = wy;
+ if (wy > _waterMaxY) _waterMaxY = wy;
+ if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z];
+ }
+ }
+ }
+
float hRight = surfaceRight - exactSurfaceY;
float hFwd = surfaceFwd - exactSurfaceY;
float slopeX = hRight / Constants.VOXEL_SCALE;
@@ -275,6 +330,7 @@ namespace IslaApocalypse.Server
} // End of Z loop
} // End of X loop
+ if (newChunk.HasAnyWater) _chunksWithWater++;
_activeChunks.Add(chunkCoord, newChunk);
var renderer = new IslaApocalypse.Client.ChunkRenderer();
@@ -282,6 +338,21 @@ namespace IslaApocalypse.Server
renderer.RenderChunk(newChunk);
}
+ ///
+ /// A water body's flat surface level, from the blueprint's WBTB table. Only
+ /// used as the fallback when a column is flagged wet by WBID but its WSRF
+ /// entry is the no-water sentinel — WSRF is the per-pixel authority, this is
+ /// the per-body one. Returns -1 if the body is unknown, which the caller
+ /// reads as "leave this column dry" rather than guessing a level.
+ ///
+ private float BodyLevel(ushort bodyId)
+ {
+ if (_blueprint.WaterBodies == null) return -1f;
+ foreach (var body in _blueprint.WaterBodies)
+ if (body.Id == bodyId) return body.SurfaceLevel;
+ return -1f;
+ }
+
///
/// 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