Rivers and bridges: water and lava tiles, bridge tool, burning in lava

- New tile kinds (appended for the savegame codec): water, lava, wooden bridge, stone bridge, core shard
- Sunken river beds with banks, bridge decks with rails, lava glow and lights
- Bridge tool (key U, rooms category): bridges grow from own ground, 30 gold/tile over water, 75 over lava
- Selling a bridge drops the units on it into the river (water: swim to the bank, lava: burn)
- Cannon blasts and punches knock units back; into lava = burnt (heroes still pay their bounty)
- Heroes cannot path through rivers; texts EN/DE; tests for bridges and lava

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 10:32:40 +01:00
1 parent af6be62805
commit 75dc445d3b
9 files changed
+536 -22

No files matched your search

+94 -1
View File
@@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95;
const CHUNK: i32 = 16;
pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0;
const ATLAS_H: f32 = 96.0;
const ATLAS_H: f32 = 128.0;
const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera
@@ -358,6 +358,9 @@ fn ambient_field(map: &Map) -> Vec<f32> {
(false, Owner::Hero) => 0.6,
_ => 0.0,
};
if t.river() == Some(Kind::Lava) {
f[i] = f[i].max(0.8); // lava lights up its surroundings
}
if t.kind == Kind::Core {
f[i] = 1.0;
}
@@ -401,6 +404,8 @@ fn solid_at(map: &Map, x: i32, z: i32) -> bool {
}
const OUTLINE_W: f32 = 0.035;
const LIQ_D: f32 = 0.3; // depth of the river surface below the floor
const DECK_Y: f32 = 0.03; // height of bridge decks
const OUTLINE_C: [u8; 4] = [8, 5, 6, 150];
// Displaced points along a straight edge (same displacement as the wall grids -> lines follow the surface)
@@ -487,6 +492,75 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var, t.reinforced);
let (ua, ub) = cell_uv(tex);
let (xf, zf) = (x as f32, z as f32);
if let Some(river) = t.river() {
// ---- river bed: sunken surface, banks where the ground or a wall begins
let lava = river == Kind::Lava;
let (la, lb) = cell_uv(if lava { T_LAVA } else { T_WATER } + (t.var as u32 & 3));
let a = amb_at(amb, xf + 0.5, zf + 0.5);
let lc = if lava { col(vec3(1.0, 0.95, 0.9), 1.0) } else { col(Vec3::splat(0.25 + a * 0.9), 1.0) };
mb.quad(
[vec3(xf, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf + 1.0), vec3(xf, -LIQ_D, zf + 1.0)],
[vec2(la.x, la.y), vec2(lb.x, la.y), vec2(lb.x, lb.y), vec2(la.x, lb.y)],
[lc; 4],
[Vec3::Y; 4],
);
let (fa, fb) = cell_uv(T_FACE_EARTH);
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
if map.get(x + dx, z + dz).map_or(false, |o| o.river().is_some()) {
continue;
}
// edge seen from inside the river: from a to b, facing back into this tile
let (p, q) = match d {
0 => (vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf)),
1 => (vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0)),
2 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)),
_ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)),
};
let inward = vec3(-dx as f32, 0.0, -dz as f32);
let bc = col(Vec3::splat(if lava { 0.7 } else { 0.18 + a * 0.5 }), 1.0);
mb.quad(
[p, q, q - Vec3::Y * LIQ_D, p - Vec3::Y * LIQ_D],
[vec2(fa.x, fa.y), vec2(fb.x, fa.y), vec2(fb.x, fb.y), vec2(fa.x, fb.y)],
[bc; 4],
[inward; 4],
);
}
if t.bridge() {
// deck: planks across the direction of the bridge, rails along it
let run_x = |dx: i32| map.get(x + dx, z).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
let run_z = |dz: i32| map.get(x, z + dz).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
let along_x = (run_x(-1) as i32 + run_x(1) as i32) > (run_z(-1) as i32 + run_z(1) as i32);
let (ba, bb) = cell_uv(tex);
let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), 1.0);
let uv = if along_x {
[vec2(ba.x, ba.y), vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y)]
} else {
[vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, bb.y), vec2(ba.x, bb.y)]
};
mb.quad([vec3(xf, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf, DECK_Y, zf + 1.0)], uv, [dc; 4], [Vec3::Y; 4]);
// deck edges above open river
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
if !map.get(x + dx, z + dz).map_or(false, |o| o.liquid()) {
continue;
}
let (p, q) = match d {
0 => (vec3(xf + 1.0, DECK_Y, zf), vec3(xf, DECK_Y, zf)),
1 => (vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf)),
2 => (vec3(xf, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf + 1.0)),
_ => (vec3(xf, DECK_Y, zf), vec3(xf, DECK_Y, zf + 1.0)),
};
let out = vec3(dx as f32, 0.0, dz as f32);
let ec = col(Vec3::splat(0.3), 1.0);
mb.quad([p, q, q - Vec3::Y * 0.14, p - Vec3::Y * 0.14], [vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, ba.y + 0.002), vec2(ba.x, ba.y + 0.002)], [ec; 4], [out; 4]);
ln.strip(p + out * 0.01, q + out * 0.01, Vec3::Y, OUTLINE_W, OUTLINE_C);
}
}
continue;
}
if t.kind == Kind::Shard {
let dc = col(Vec3::splat(1.0), 1.0);
mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.8, D_SHARD, dc);
}
if !t.solid() {
// floor with corner ambient occlusion
let corners = [(0, 0), (1, 0), (1, 1), (0, 1)];
@@ -807,6 +881,20 @@ impl R3d {
let (gx, gz) = g.gate_center();
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
}
// glowing lava near the camera (every third tile)
let r = (cam.zoom * 0.9) as i32;
let (ccx, ccz) = (cam.tx as i32, cam.tz as i32);
for z in (ccz - r).max(0)..(ccz + r).min(mh() as i32) {
if z % 3 != 0 {
continue;
}
for x in (ccx - r).max(0)..(ccx + r).min(mw() as i32) {
if x % 3 == 0 && g.map.t[idx(x, z)].kind == Kind::Lava {
let fl = 0.8 + 0.2 * (t * 2.0 + (x * 7 + z * 3) as f32).sin();
push(vec3(x as f32 + 0.5, 0.3, z as f32 + 0.5), 4.0, vec3(1.0, 0.45, 0.12), 0.9 * fl, 0.0);
}
}
}
let pulse = 0.5 + 0.5 * (t * 4.0).sin();
for &(ex, ey) in &g.planned_entries {
push(vec3(ex as f32 + 0.5, 1.2, ey as f32 + 0.5), 5.0, vec3(1.0, 0.12, 0.08), 0.6 + 1.0 * pulse, 30.0);
@@ -1453,5 +1541,10 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Torture => [120, 40, 40, 255],
Kind::Tower => [255, 140, 60, 255],
Kind::Temple => [200, 110, 50, 255],
Kind::Water => [40, 80, 150, 255],
Kind::Lava => [250, 110, 30, 255],
Kind::Bridge => [150, 110, 60, 255],
Kind::LavaBridge => [130, 120, 115, 255],
Kind::Shard => [255, 60, 120, 255],
}
}