Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions client/src/GameLogic/ArenaBuilders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ public ArenaLayout Build(int seed)
{
var (materials, bushes) = ArenaAuthor.Field();

// Two horizontal lava rivers spanning the field, each with four bridge crossings.
// Two horizontal lava rivers spanning the field, each with four bridge crossings. Every
// crossing is three cells wide — a single cell made brushing lava almost unavoidable
// (owner feedback 2026-07-17).
foreach (var riverY in new[] { 13, 32 })
{
for (var x = 3; x <= 72; x++)
Expand All @@ -125,7 +127,10 @@ public ArenaLayout Build(int seed)

foreach (var bridgeX in new[] { 12, 28, 46, 62 })
{
materials[bridgeX, riverY] = CellMaterial.Bridge;
for (var dx = -1; dx <= 1; dx++)
{
materials[bridgeX + dx, riverY] = CellMaterial.Bridge;
}
}
}

Expand All @@ -135,7 +140,10 @@ public ArenaLayout Build(int seed)
materials[38, y] = CellMaterial.Lava;
}

materials[38, 22] = CellMaterial.Bridge;
for (var y = 21; y <= 23; y++)
{
materials[38, y] = CellMaterial.Bridge;
}

// Basalt outcrops (steel) for hard cover on the rims.
foreach (var (cx, cy) in new[] { (18, 6), (58, 6), (18, 39), (58, 39), (38, 6), (38, 39) })
Expand Down
66 changes: 55 additions & 11 deletions client/src/Presentation/Arena/Terrain3DView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,23 +177,67 @@ private void AddWater(NVector2 centre, string name) =>
MaterialOverride = new StandardMaterial3D { AlbedoColor = new Color(0.20f, 0.42f, 0.66f), Roughness = 0.3f },
});

// A molten lava pool: a glowing red plane sitting near the ground, emissive so it reads as hot even
// in shadow. Flush enough that a bridge tile (drawn just above) sits over it.
// Molten lava: dark crust broken by glowing, slowly-flowing cracks. Two layers of procedural value
// noise scroll in different directions (so the surface churns rather than sliding as one sticker),
// and the crack glow pulses. Sampled in WORLD space so the flow runs seamlessly across neighbouring
// cells. Unshaded with zero texture fetches — cheap enough for the Compatibility renderer
// (WASM/mobile); unshaded also keeps it reading as hot even in shadow.
private const string LavaShaderCode = @"
shader_type spatial;
render_mode unshaded;
uniform float cell_size = 64.0;
varying vec2 wpos;
void vertex() {
wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
}
float vhash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(vhash(i), vhash(i + vec2(1.0, 0.0)), u.x),
mix(vhash(i + vec2(0.0, 1.0)), vhash(i + vec2(1.0, 1.0)), u.x), u.y);
}
void fragment() {
vec2 p = wpos / cell_size;
float a = vnoise((p * 2.2) + (TIME * vec2(0.14, 0.05)));
float b = vnoise((p * 5.1) - (TIME * vec2(0.06, 0.11)));
float n = (a * 0.65) + (b * 0.35);
float molten = smoothstep(0.42, 0.72, n);
float pulse = 0.8 + (0.2 * sin((TIME * 1.6) + (n * 12.0)));
vec3 crust = vec3(0.10, 0.03, 0.02);
vec3 hot = mix(vec3(0.95, 0.28, 0.02), vec3(1.0, 0.85, 0.30), smoothstep(0.75, 0.95, n));
ALBEDO = mix(crust, hot * pulse, molten);
}";

private static Shader _lavaShader = null!;
private ShaderMaterial? _lavaMaterial;

// One material shared by every lava cell in the arena — world-space sampling makes them one
// continuous surface, and sharing lets the renderer batch them.
private ShaderMaterial LavaMaterial()
{
if (_lavaMaterial is null)
{
_lavaShader ??= new Shader { Code = LavaShaderCode };
_lavaMaterial = new ShaderMaterial { Shader = _lavaShader };
_lavaMaterial.SetShaderParameter("cell_size", _tileSize);
}

return _lavaMaterial;
}

// A molten lava pool sitting near the ground, flush enough that a bridge tile (drawn just above)
// sits over it.
private void AddLava(NVector2 centre, string name) =>
AddChild(new MeshInstance3D
{
Name = name,
Mesh = new PlaneMesh { Size = new Vector2(_tileSize, _tileSize) },
Position = new Vector3(centre.X, 0.6f, centre.Y),
MaterialOverride = new StandardMaterial3D
{
AlbedoColor = new Color(0.85f, 0.18f, 0.05f),
EmissionEnabled = true,
Emission = new Color(0.95f, 0.30f, 0.05f),
EmissionEnergyMultiplier = 2.5f,
Roughness = 0.6f,
SpecularMode = BaseMaterial3D.SpecularModeEnum.Disabled,
},
MaterialOverride = LavaMaterial(),
});

// A flat road tile flush with the ground (the bridge crossing over water).
Expand Down
39 changes: 39 additions & 0 deletions client/tests/GameLogic/ArenaBuildersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,45 @@ public void Volcano_HasLava_CrossedByABridgeOverLava()
Assert.True(bridgeOverLava, "volcano must have at least one Bridge crossing lava");
}

// Owner feedback: single-cell crossings made it too easy to clip lava — every crossing must be a
// run of at least three Bridge cells along its river, so a tank has room to drive across.
[Fact]
public void Volcano_EveryLavaCrossing_IsAtLeastThreeCellsWide()
{
var layout = BuildOrFail("Volcano");
var mats = layout.Map.Materials;
for (var x = 0; x < layout.Map.Width; x++)
{
for (var y = 0; y < layout.Map.Height; y++)
{
if (mats[x, y] != CellMaterial.Bridge)
{
continue;
}

var run = System.Math.Max(RunLength(mats, x, y, 1, 0), RunLength(mats, x, y, 0, 1));
Assert.True(run >= 3, $"bridge at ({x},{y}) is only {run} cell(s) wide");
}
}
}

// The length of the contiguous straight run of Bridge cells through (x, y) along (dx, dy).
private static int RunLength(CellMaterial[,] mats, int x, int y, int dx, int dy)
{
var run = 1;
for (var i = 1; mats[x + (i * dx), y + (i * dy)] == CellMaterial.Bridge; i++)
{
run++;
}

for (var i = 1; mats[x - (i * dx), y - (i * dy)] == CellMaterial.Bridge; i++)
{
run++;
}

return run;
}

[Fact]
public void City_IsAGridOfBuildingBlocks()
{
Expand Down
Loading