Title demo: lava and water rivers around a rock tongue, pillars and walls

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 15:11:36 +01:00
1 parent 1094c7c5f2
commit 2a6892f9a6
1 file changed
+97 -34
+97 -34
View File
@@ -1,5 +1,5 @@
// The endless battle behind the title menu: the dungeon's creatures and the knights fight at // The endless battle behind the title menu: the dungeon's creatures and the knights fight on
// a bridge over an underground river. Both sides get balanced reinforcements, destroyed towers // 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. // are rebuilt after a while. Nothing here is ever saved.
use crate::game::*; use crate::game::*;
@@ -8,8 +8,13 @@ use crate::render3d::Cam;
const W: usize = 72; const W: usize = 72;
const H: usize = 48; const H: usize = 48;
const RIVER_X: i32 = 34; // first water column (four tiles wide) const RIVER_X: i32 = 35; // middle column of the rivers (five tiles wide, winding a little)
const BRIDGE_Y: i32 = 23; // first bridge row (three tiles wide) 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 PER_SIDE: usize = 9; // at most this many units per side
const REBUILD: f32 = 25.0; // seconds until a destroyed tower stands again 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); let mut g = Game::from_map(map, seed);
g.ents.clear(); g.ents.clear();
let mut rng = Rng(seed | 1); let mut rng = Rng(seed | 1);
// a wide cave on both banks: the dungeon's ground on the left, knight ground on the right let phase = rng.f() * 6.0;
for y in 3..H as i32 - 3 { // a wide cave with a ragged outline: the dungeon's ground on the left, knight ground on the right
for x in 3..W as i32 - 3 { for y in 2..H as i32 - 2 {
let cy = (y - H as i32 / 2) as f32 / (H as f32 * 0.42); for x in 2..W as i32 - 2 {
let cx = (x - W as i32 / 2) as f32 / (W as f32 * 0.47); let cy = (y - H as i32 / 2) as f32 / (H as f32 * 0.44);
let edge = 1.0 + (rng.f() - 0.5) * 0.12; let cx = (x - W as i32 / 2) as f32 / (W as f32 * 0.48);
if cx * cx + cy * cy > edge { 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; continue;
} }
let t = &mut g.map.t[idx(x, y)]; let t = &mut g.map.t[idx(x, y)];
if matches!(t.kind, Kind::Core | Kind::Treasury) { if matches!(t.kind, Kind::Core | Kind::Treasury) {
continue; continue;
} }
let m = river_mid(y);
t.kind = Kind::Floor; t.kind = Kind::Floor;
t.gold = 0; 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 // the rivers from top to bottom: lava north of the tongue, water south of it; they cut
for y in 2..H as i32 - 2 { // through the rock to the map edge
for x in RIVER_X..RIVER_X + 4 { 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)]; let t = &mut g.map.t[idx(x, y)];
t.owner = Owner::None; t.owner = Owner::None;
t.gold = 0; 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 { // the tongue's edges are uneven: a bulge into the lava on the west, into the water on the east
for x in RIVER_X..RIVER_X + 4 { 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)].owner = Owner::Player; g.map.t[idx(x, y)].kind = Kind::Floor;
} }
// 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;
} }
// 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 {
let t = &mut g.map.t[idx(x, y)]; let t = &mut g.map.t[idx(x, y)];
if t.kind == Kind::Floor && t.owner == Owner::Hero { if t.kind == Kind::Floor {
t.kind = Kind::Lava; t.kind = if rock { Kind::Rock } else { Kind::Earth };
t.owner = Owner::None; 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 // 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 y in y0..=y1 {
for x in x0..=x1 { for x in x0..=x1 {
let t = &mut g.map.t[idx(x, y)]; 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![ let towers = vec![
(RIVER_X - 4, BRIDGE_Y - 3, TowerKind::Arrow, false, 0.0), (river_mid(n) - 4, n, TowerKind::Arrow, false, 0.0),
(RIVER_X - 4, BRIDGE_Y + 5, TowerKind::Cannon, false, 0.0), (river_mid(s_) - 4, s_, TowerKind::Cannon, false, 0.0),
(RIVER_X - 8, BRIDGE_Y + 1, TowerKind::Magic, false, 0.0), (RIVER_X - 9, BRIDGE_Y + 1, TowerKind::Magic, false, 0.0),
(RIVER_X + 7, BRIDGE_Y - 3, TowerKind::Arrow, true, 0.0), (river_mid(n) + 4, n, TowerKind::Arrow, true, 0.0),
(RIVER_X + 7, BRIDGE_Y + 5, TowerKind::Cannon, true, 0.0), (river_mid(s_) + 4, s_, TowerKind::Arrow, true, 0.0),
(RIVER_X + 11, BRIDGE_Y + 1, 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.demo = Some(Demo { spawn_t: 0.0, towers, rng: Rng(seed.wrapping_mul(747796405) | 1) });
g.gate_hp = 0.0; g.gate_hp = 0.0;
@@ -116,7 +152,7 @@ impl Game {
g.demo_spawn(true); g.demo_spawn(true);
g.demo_spawn(false); 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; cam.zoom = 16.0;
(g, cam) (g, cam)
} }
@@ -297,6 +333,33 @@ impl Game {
mod tests { mod tests {
use super::*; 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] #[test]
fn the_battle_goes_on() { fn the_battle_goes_on() {
let (mut g, _) = Game::new_demo(7); let (mut g, _) = Game::new_demo(7);