From 2a6892f9a63f6caf8fa854875913ffc034308e2b Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 15:11:36 +0100 Subject: [PATCH] Title demo: lava and water rivers around a rock tongue, pillars and walls Co-Authored-By: Claude Opus 5.5 --- src/demo.rs | 133 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 35 deletions(-) diff --git a/src/demo.rs b/src/demo.rs index 392ce65..2e0a7a0 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -1,5 +1,5 @@ -// The endless battle behind the title menu: the dungeon's creatures and the knights fight at -// a bridge over an underground river. Both sides get balanced reinforcements, destroyed towers +// The endless battle behind the title menu: the dungeon's creatures and the knights fight on +// a tongue of rock between a lava river (north) and an underground river (south). Both sides get balanced reinforcements, destroyed towers // are rebuilt after a while. Nothing here is ever saved. use crate::game::*; @@ -8,8 +8,13 @@ use crate::render3d::Cam; const W: usize = 72; const H: usize = 48; -const RIVER_X: i32 = 34; // first water column (four tiles wide) -const BRIDGE_Y: i32 = 23; // first bridge row (three tiles wide) +const RIVER_X: i32 = 35; // middle column of the rivers (five tiles wide, winding a little) +const BRIDGE_Y: i32 = 22; // first row of the rock tongue across the rivers (three rows) + +// middle column of the river in a row: it winds around RIVER_X +fn river_mid(y: i32) -> i32 { + RIVER_X + (1.6 * (y as f32 * 0.3).sin()).round() as i32 +} const PER_SIDE: usize = 9; // at most this many units per side const REBUILD: f32 = 25.0; // seconds until a destroyed tower stands again @@ -41,53 +46,81 @@ impl Game { let mut g = Game::from_map(map, seed); g.ents.clear(); let mut rng = Rng(seed | 1); - // a wide cave on both banks: the dungeon's ground on the left, knight ground on the right - for y in 3..H as i32 - 3 { - for x in 3..W as i32 - 3 { - let cy = (y - H as i32 / 2) as f32 / (H as f32 * 0.42); - let cx = (x - W as i32 / 2) as f32 / (W as f32 * 0.47); - let edge = 1.0 + (rng.f() - 0.5) * 0.12; - if cx * cx + cy * cy > edge { + let phase = rng.f() * 6.0; + // a wide cave with a ragged outline: the dungeon's ground on the left, knight ground on the right + for y in 2..H as i32 - 2 { + for x in 2..W as i32 - 2 { + let cy = (y - H as i32 / 2) as f32 / (H as f32 * 0.44); + let cx = (x - W as i32 / 2) as f32 / (W as f32 * 0.48); + let a = cy.atan2(cx); + let edge = 1.0 + 0.11 * (3.0 * a + phase).sin() + 0.07 * (7.0 * a - phase).sin() + (rng.f() - 0.5) * 0.06; + if cx * cx + cy * cy > edge * edge { continue; } let t = &mut g.map.t[idx(x, y)]; if matches!(t.kind, Kind::Core | Kind::Treasury) { continue; } + let m = river_mid(y); t.kind = Kind::Floor; t.gold = 0; - t.owner = if x < RIVER_X { Owner::Player } else if x >= RIVER_X + 4 { Owner::Hero } else { Owner::None }; + t.reinforced = false; + t.owner = if x < m - 2 { Owner::Player } else if x > m + 2 { Owner::Hero } else { Owner::None }; } } - // the river from top to bottom, and the bridge across - for y in 2..H as i32 - 2 { - for x in RIVER_X..RIVER_X + 4 { + // the rivers from top to bottom: lava north of the tongue, water south of it; they cut + // through the rock to the map edge + for y in 1..H as i32 - 1 { + let m = river_mid(y); + for x in m - 2..=m + 2 { let t = &mut g.map.t[idx(x, y)]; t.owner = Owner::None; t.gold = 0; - t.kind = if (BRIDGE_Y..BRIDGE_Y + 3).contains(&y) { Kind::Bridge } else { Kind::Water }; + t.reinforced = false; + t.kind = if y < BRIDGE_Y { Kind::Lava } else if y >= BRIDGE_Y + 3 { Kind::Water } else { Kind::Floor }; } } - for y in BRIDGE_Y..BRIDGE_Y + 3 { - for x in RIVER_X..RIVER_X + 4 { - g.map.t[idx(x, y)].owner = Owner::Player; - } + // the tongue's edges are uneven: a bulge into the lava on the west, into the water on the east + for (x, y) in [(river_mid(BRIDGE_Y - 1) - 2, BRIDGE_Y - 1), (river_mid(BRIDGE_Y - 1) - 1, BRIDGE_Y - 1), (river_mid(BRIDGE_Y + 3) + 1, BRIDGE_Y + 3), (river_mid(BRIDGE_Y + 3) + 2, BRIDGE_Y + 3)] { + g.map.t[idx(x, y)].kind = Kind::Floor; } - // lava pools in front of the human arrow towers, with gaps so the path to the bridge stays open - for (x0, y0, x1, y1) in [(RIVER_X + 5, BRIDGE_Y - 4, RIVER_X + 6, BRIDGE_Y - 3), (RIVER_X + 9, BRIDGE_Y - 1, RIVER_X + 10, BRIDGE_Y)] { - for y in y0..=y1 { - for x in x0..=x1 { + // rock and earth: pillars in the cave and shoulders along the banks + let blobs: [(f32, f32, f32, bool); 13] = [ + // dungeon side: earth + (12.0, 10.5, 1.7, false), + (23.5, 8.5, 1.4, false), + (19.0, 34.0, 1.9, false), + (27.5, 38.0, 1.3, false), + (6.5, 24.5, 1.1, false), + (29.0, 14.0, 1.2, false), + // knight side: rock + (47.5, 9.5, 1.5, true), + (58.0, 13.0, 1.8, true), + (52.0, 36.5, 1.7, true), + (62.0, 29.5, 1.3, true), + (44.5, 39.0, 1.1, true), + (42.0, 31.5, 1.2, true), + // a stalagmite on the tongue's west end, beside the path + (31.5, BRIDGE_Y as f32 - 1.5, 0.75, false), + ]; + for &(bx, by, r, rock) in blobs.iter() { + for y in (by - r - 1.0) as i32..=(by + r + 1.0) as i32 { + for x in (bx - r - 1.0) as i32..=(bx + r + 1.0) as i32 { + let d = ((x as f32 + 0.5 - bx).powi(2) + (y as f32 + 0.5 - by).powi(2)).sqrt(); + if d > r + (rng.f() - 0.5) * 0.5 || !inb(x, y) { + continue; + } let t = &mut g.map.t[idx(x, y)]; - if t.kind == Kind::Floor && t.owner == Owner::Hero { - t.kind = Kind::Lava; + if t.kind == Kind::Floor { + t.kind = if rock { Kind::Rock } else { Kind::Earth }; t.owner = Owner::None; - t.gold = 0; + t.reinforced = !rock && x < RIVER_X - 6; } } } } // a few rooms on the dungeon side for the look of it - for (x0, y0, x1, y1, k) in [(8, 13, 13, 17, Kind::Lair), (8, 30, 13, 34, Kind::Training), (17, 12, 21, 15, Kind::Farm)] { + for (x0, y0, x1, y1, k) in [(6, 15, 11, 19, Kind::Lair), (9, 29, 14, 33, Kind::Training), (16, 13, 20, 16, Kind::Farm)] { for y in y0..=y1 { for x in x0..=x1 { let t = &mut g.map.t[idx(x, y)]; @@ -97,13 +130,16 @@ impl Game { } } } + // towers on both banks: the arrow towers stand by the lava and by the water, so missed + // arrows come down in both rivers + let (n, s_) = (BRIDGE_Y - 4, BRIDGE_Y + 6); let towers = vec![ - (RIVER_X - 4, BRIDGE_Y - 3, TowerKind::Arrow, false, 0.0), - (RIVER_X - 4, BRIDGE_Y + 5, TowerKind::Cannon, false, 0.0), - (RIVER_X - 8, BRIDGE_Y + 1, TowerKind::Magic, false, 0.0), - (RIVER_X + 7, BRIDGE_Y - 3, TowerKind::Arrow, true, 0.0), - (RIVER_X + 7, BRIDGE_Y + 5, TowerKind::Cannon, true, 0.0), - (RIVER_X + 11, BRIDGE_Y + 1, TowerKind::Arrow, true, 0.0), + (river_mid(n) - 4, n, TowerKind::Arrow, false, 0.0), + (river_mid(s_) - 4, s_, TowerKind::Cannon, false, 0.0), + (RIVER_X - 9, BRIDGE_Y + 1, TowerKind::Magic, false, 0.0), + (river_mid(n) + 4, n, TowerKind::Arrow, true, 0.0), + (river_mid(s_) + 4, s_, TowerKind::Arrow, true, 0.0), + (RIVER_X + 9, BRIDGE_Y + 1, TowerKind::Cannon, true, 0.0), ]; g.demo = Some(Demo { spawn_t: 0.0, towers, rng: Rng(seed.wrapping_mul(747796405) | 1) }); g.gate_hp = 0.0; @@ -116,7 +152,7 @@ impl Game { g.demo_spawn(true); g.demo_spawn(false); } - let mut cam = Cam::new(RIVER_X as f32 + 2.0, BRIDGE_Y as f32 + 1.5); + let mut cam = Cam::new(RIVER_X as f32 + 0.5, BRIDGE_Y as f32 + 1.5); cam.zoom = 16.0; (g, cam) } @@ -297,6 +333,33 @@ impl Game { mod tests { use super::*; + #[test] + fn the_map_has_both_rivers_and_a_tongue() { + let (g, _) = Game::new_demo(7); + for y in 0..H as i32 { + let row: String = (0..W as i32) + .map(|x| match g.map.t[idx(x, y)].kind { + Kind::Lava => '~', + Kind::Water => '=', + Kind::Rock => '#', + Kind::Earth => '%', + Kind::Tower => 'T', + k if g.map.t[idx(x, y)].walkable() && k != Kind::Floor => 'r', + _ if g.map.t[idx(x, y)].walkable() => '.', + _ => ' ', + }) + .collect(); + println!("{}", row); + } + let m = river_mid(BRIDGE_Y + 1); + // lava borders the tongue in the north, water in the south + assert_eq!(g.map.t[idx(m, BRIDGE_Y - 2)].kind, Kind::Lava); + assert_eq!(g.map.t[idx(m, BRIDGE_Y + 4)].kind, Kind::Water); + // the tongue connects both banks + assert!(g.map.path_to(idx(m - 7, BRIDGE_Y + 1), idx(m + 6, BRIDGE_Y + 1)).is_some(), "across the tongue"); + assert_eq!(g.towers.len(), 6); + } + #[test] fn the_battle_goes_on() { let (mut g, _) = Game::new_demo(7);