From 4b57157e9c6470c4027d53f4f04c3c0c0abbdaeb Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 19:38:46 +0100 Subject: [PATCH] Title demo as a showcase: doors, every trap, rune tower, rooms, mana spring, hero gate, spells Co-Authored-By: Claude Opus 5.5 --- src/demo.rs | 163 +++++++++++++++++++++++++++++++++++++++++++++++---- src/traps.rs | 9 +++ 2 files changed, 160 insertions(+), 12 deletions(-) diff --git a/src/demo.rs b/src/demo.rs index 2e0a7a0..243b3e6 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -1,6 +1,10 @@ -// 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. +// The endless battle behind the title menu, a showcase of the game: the dungeon's creatures and +// the knights fight on a tongue of rock between a lava river (north) and an underground river +// (south). The dungeon side has a wall with a wooden, an iron and a magic door, rooms behind it +// (treasury, forge, library, lair, ...), a rune forge tower, a magic tower and a mana spring; all +// seven traps lie on the knights' way; the knights have their towers and the hero gate. Both sides +// get balanced reinforcements, destroyed towers, doors and traps come back after a while, and now +// and then the dungeon casts a spell. Nothing here is ever saved. use crate::game::*; use crate::map::*; @@ -21,11 +25,15 @@ const REBUILD: f32 = 25.0; // seconds until a destroyed tower stands again pub struct Demo { spawn_t: f32, towers: Vec<(i32, i32, TowerKind, bool, f32)>, // slot: position, kind, knight tower, time since lost + traps: Vec<(usize, crate::traps::TrapKind, f32)>, // slot: tile, trap or door, time since lost + spell_t: f32, rng: Rng, } -const CREATURES: [(EK, u32); 9] = [(EK::Kobold, 4), (EK::Spider, 3), (EK::Mage, 2), (EK::Ogre, 2), (EK::Jailer, 1), (EK::Priestess, 1), (EK::Bat, 2), (EK::Gargoyle, 1), (EK::Owl, 1)]; -const KNIGHTS: [(EK, u32); 5] = [(EK::Knight, 5), (EK::Archer, 3), (EK::Priest, 2), (EK::Dwarf, 2), (EK::Paladin, 1)]; +const WALL_X: i32 = RIVER_X - 8; // the dungeon's wall with its three doors + +const CREATURES: [(EK, u32); 12] = [(EK::Kobold, 4), (EK::Spider, 3), (EK::Mage, 2), (EK::Ogre, 2), (EK::Jailer, 1), (EK::Priestess, 1), (EK::Bat, 2), (EK::Gargoyle, 1), (EK::Owl, 1), (EK::Wyrm, 1), (EK::Cogling, 1), (EK::Revenant, 1)]; +const KNIGHTS: [(EK, u32); 8] = [(EK::Knight, 5), (EK::Archer, 3), (EK::Priest, 2), (EK::Dwarf, 2), (EK::Paladin, 1), (EK::Giant, 1), (EK::Fairy, 1), (EK::BattleMage, 1)]; fn pick(rng: &mut Rng, pool: &[(EK, u32)]) -> EK { let total: u32 = pool.iter().map(|p| p.1).sum(); @@ -119,8 +127,34 @@ impl Game { } } } - // a few rooms on the dungeon side for the look of it - 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)] { + // the dungeon's wall from top to bottom, with a door in three gaps + for y in 2..H as i32 - 2 { + let t = &mut g.map.t[idx(WALL_X, y)]; + if !t.solid() { + t.kind = Kind::Earth; + t.reinforced = true; + t.owner = Owner::Player; + } + } + let doors = [(BRIDGE_Y - 5, crate::traps::TrapKind::WoodDoor), (BRIDGE_Y + 1, crate::traps::TrapKind::IronDoor), (BRIDGE_Y + 7, crate::traps::TrapKind::MagicDoor)]; + for &(y, _) in doors.iter() { + for x in WALL_X - 1..=WALL_X + 1 { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Floor; + t.reinforced = false; + t.owner = Owner::Player; + } + } + // a few rooms on the dungeon side, the showy ones right behind the wall + for (x0, y0, x1, y1, k) in [ + (WALL_X - 5, BRIDGE_Y - 3, WALL_X - 2, BRIDGE_Y - 1, Kind::Treasury), + (WALL_X - 5, BRIDGE_Y + 3, WALL_X - 2, BRIDGE_Y + 5, Kind::Forge), + (WALL_X - 5, BRIDGE_Y - 9, WALL_X - 2, BRIDGE_Y - 7, Kind::Library), + (WALL_X - 5, BRIDGE_Y + 9, WALL_X - 2, BRIDGE_Y + 11, Kind::Temple), + (6, 15, 11, 19, Kind::Lair), + (9, 29, 14, 33, Kind::Training), + (14, 12, 18, 16, Kind::Aerie), + ] { for y in y0..=y1 { for x in x0..=x1 { let t = &mut g.map.t[idx(x, y)]; @@ -136,13 +170,47 @@ impl Game { let towers = vec![ (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), + (WALL_X + 2, BRIDGE_Y + 4, TowerKind::Magic, false, 0.0), + (WALL_X + 2, BRIDGE_Y - 2, TowerKind::Rune, 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), + (RIVER_X + 8, BRIDGE_Y - 7, 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; + // treasure in the treasury, a mana spring by the wall + for t in g.map.t.iter_mut().filter(|t| t.kind == Kind::Treasury) { + t.gold = 300; + } + let spring = idx(WALL_X - 3, BRIDGE_Y + 1); + g.map.t[spring].kind = Kind::ManaSpring; + g.map.t[spring].owner = Owner::Player; + // the hero gate on the knights' side + let (gx, gy) = (RIVER_X + 11, BRIDGE_Y); + for y in gy..gy + 3 { + for x in gx..gx + 3 { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Gate; + t.owner = Owner::Hero; + } + } + g.map.gate = (gx, gy); + // the doors, and every trap on the knights' way: on the tongue and before the wall + use crate::traps::TrapKind as T; + let mut traps: Vec<(usize, T, f32)> = doors.iter().map(|&(y, k)| (idx(WALL_X, y), k, 0.0)).collect(); + let m = river_mid(BRIDGE_Y + 1); + for (x, y, k) in [ + (m - 1, BRIDGE_Y, T::Spikes), + (m + 1, BRIDGE_Y + 2, T::Poison), + (m, BRIDGE_Y + 1, T::Fire), + (m - 2, BRIDGE_Y + 2, T::Frost), + (m + 2, BRIDGE_Y, T::Mine), + (WALL_X + 3, BRIDGE_Y + 1, T::Roller), + (WALL_X + 4, BRIDGE_Y - 4, T::Alarm), + ] { + traps.push((idx(x, y), k, 0.0)); + } + g.demo = Some(Demo { spawn_t: 0.0, towers, traps, spell_t: 6.0, rng: Rng(seed.wrapping_mul(747796405) | 1) }); + g.place_all_traps_of_demo(); + g.gate_hp = GATE_HP; g.gate_on = false; g.mana = MANA_MAX; g.place_all_towers_of_demo(); @@ -157,6 +225,29 @@ impl Game { (g, cam) } + // a trap or door of the demo on its tile (again) + fn demo_trap(&mut self, i: usize, kind: crate::traps::TrapKind) { + use crate::traps::*; + if self.traps.iter().any(|t| t.tile == i) { + return; + } + if kind.door() { + self.map.t[i].kind = Kind::Door; + } + self.map.t[i].owner = Owner::Player; + let id = self.next_id; + self.next_id += 1; + self.traps.push(Trap { id, kind, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false }); + } + + fn place_all_traps_of_demo(&mut self) { + let Some(d) = self.demo.as_ref() else { return }; + let slots: Vec<(usize, crate::traps::TrapKind)> = d.traps.iter().map(|s| (s.0, s.1)).collect(); + for (i, k) in slots { + self.demo_trap(i, k); + } + } + fn place_all_towers_of_demo(&mut self) { let Some(d) = self.demo.as_ref() else { return }; let slots: Vec<(i32, i32, TowerKind, bool)> = d.towers.iter().map(|s| (s.0, s.1, s.2, s.3)).collect(); @@ -207,7 +298,41 @@ impl Game { self.mana = MANA_MAX; self.update_units(dt); self.update_towers(dt); + self.demo_traps(dt); let Some(mut d) = self.demo.take() else { return }; + // destroyed doors and wrecked traps come back too + let mut retrap = Vec::new(); + for s in d.traps.iter_mut() { + if self.traps.iter().any(|t| t.tile == s.0) { + s.2 = 0.0; + } else { + s.2 += dt; + if s.2 >= REBUILD { + s.2 = 0.0; + retrap.push((s.0, s.1)); + } + } + } + // now and then the dungeon casts a spell into the fight + d.spell_t -= dt; + let mut spell = None; + if d.spell_t <= 0.0 { + d.spell_t = 9.0 + d.rng.f() * 6.0; + use crate::spells::Spell; + let pick = d.rng.next() % 4; + let knights: Vec<(f32, f32)> = self.ents.iter().filter(|e| e.alive && e.active_hero()).map(|e| (e.x, e.y)).collect(); + let mine: Vec<(f32, f32)> = self.ents.iter().filter(|e| e.alive && !e.hero && e.side == 0).map(|e| (e.x, e.y)).collect(); + let (list, sp) = match pick { + 0 => (&knights, Spell::Quake), + 1 => (&knights, Spell::Plague), + 2 => (&mine, Spell::Haste), + _ => (&mine, Spell::Stoneskin), + }; + if !list.is_empty() { + let at = list[d.rng.next() as usize % list.len()]; + spell = Some((sp, at)); + } + } // reinforcements: the weaker side gets the next unit d.spawn_t -= dt; let mut want = None; @@ -243,6 +368,16 @@ impl Game { if let Some(creature) = want { self.demo_spawn(creature); } + for (i, k) in retrap { + let blocked = self.ents.iter().any(|e| e.alive && e.tile() == i); + if !blocked { + self.demo_trap(i, k); + } + } + if let Some((sp, (x, y))) = spell { + self.mana = MANA_MAX; + self.cast(sp, x, y); + } for (x, y, kind, hero) in rebuild { // nobody may stand on the spot let blocked = self.ents.iter().any(|e| e.alive && e.tile() == idx(x, y)); @@ -344,6 +479,9 @@ mod tests { Kind::Rock => '#', Kind::Earth => '%', Kind::Tower => 'T', + Kind::Door => 'D', + Kind::Gate => 'G', + Kind::ManaSpring => 'M', k if g.map.t[idx(x, y)].walkable() && k != Kind::Floor => 'r', _ if g.map.t[idx(x, y)].walkable() => '.', _ => ' ', @@ -357,7 +495,8 @@ mod tests { 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); + assert_eq!(g.towers.len(), 7); + assert!(g.traps.len() >= 10, "doors and every trap"); } #[test] diff --git a/src/traps.rs b/src/traps.rs index ef460e2..2b0dce2 100644 --- a/src/traps.rs +++ b/src/traps.rs @@ -381,6 +381,15 @@ impl Game { } // armed traps go off when enemies come close + // the title demo: its armed traps count down and go off (there is no forge economy there) + pub(crate) fn demo_traps(&mut self, dt: f32) { + for t in self.traps.iter_mut() { + t.fired += dt; + t.cd -= dt; + } + self.update_traps(dt); + } + fn update_traps(&mut self, dt: f32) { // poison clouds and rolling boulders for c in self.clouds.iter_mut() {