Title demo as a showcase: doors, every trap, rune tower, rooms, mana spring, hero gate, spells

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 19:38:46 +01:00
1 parent fa61a00e9e
commit 4b57157e9c
2 files changed
+160 -12

No files matched your search

+151 -12
View File
@@ -1,6 +1,10 @@
// The endless battle behind the title menu: the dungeon's creatures and the knights fight on // The endless battle behind the title menu, a showcase of the game: the dungeon's creatures and
// a tongue of rock between a lava river (north) and an underground river (south). Both sides get balanced reinforcements, destroyed towers // the knights fight on a tongue of rock between a lava river (north) and an underground river
// are rebuilt after a while. Nothing here is ever saved. // (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::game::*;
use crate::map::*; use crate::map::*;
@@ -21,11 +25,15 @@ const REBUILD: f32 = 25.0; // seconds until a destroyed tower stands again
pub struct Demo { pub struct Demo {
spawn_t: f32, spawn_t: f32,
towers: Vec<(i32, i32, TowerKind, bool, f32)>, // slot: position, kind, knight tower, time since lost 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, 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 WALL_X: i32 = RIVER_X - 8; // the dungeon's wall with its three doors
const KNIGHTS: [(EK, u32); 5] = [(EK::Knight, 5), (EK::Archer, 3), (EK::Priest, 2), (EK::Dwarf, 2), (EK::Paladin, 1)];
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 { fn pick(rng: &mut Rng, pool: &[(EK, u32)]) -> EK {
let total: u32 = pool.iter().map(|p| p.1).sum(); 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 // the dungeon's wall from top to bottom, with a door in three gaps
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 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 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)];
@@ -136,13 +170,47 @@ impl Game {
let towers = vec![ let towers = vec![
(river_mid(n) - 4, n, TowerKind::Arrow, false, 0.0), (river_mid(n) - 4, n, TowerKind::Arrow, false, 0.0),
(river_mid(s_) - 4, s_, TowerKind::Cannon, 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(n) + 4, n, TowerKind::Arrow, true, 0.0),
(river_mid(s_) + 4, s_, 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) }); // treasure in the treasury, a mana spring by the wall
g.gate_hp = 0.0; 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.gate_on = false;
g.mana = MANA_MAX; g.mana = MANA_MAX;
g.place_all_towers_of_demo(); g.place_all_towers_of_demo();
@@ -157,6 +225,29 @@ impl Game {
(g, cam) (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) { fn place_all_towers_of_demo(&mut self) {
let Some(d) = self.demo.as_ref() else { return }; 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(); 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.mana = MANA_MAX;
self.update_units(dt); self.update_units(dt);
self.update_towers(dt); self.update_towers(dt);
self.demo_traps(dt);
let Some(mut d) = self.demo.take() else { return }; 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 // reinforcements: the weaker side gets the next unit
d.spawn_t -= dt; d.spawn_t -= dt;
let mut want = None; let mut want = None;
@@ -243,6 +368,16 @@ impl Game {
if let Some(creature) = want { if let Some(creature) = want {
self.demo_spawn(creature); 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 { for (x, y, kind, hero) in rebuild {
// nobody may stand on the spot // nobody may stand on the spot
let blocked = self.ents.iter().any(|e| e.alive && e.tile() == idx(x, y)); let blocked = self.ents.iter().any(|e| e.alive && e.tile() == idx(x, y));
@@ -344,6 +479,9 @@ mod tests {
Kind::Rock => '#', Kind::Rock => '#',
Kind::Earth => '%', Kind::Earth => '%',
Kind::Tower => 'T', 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', k if g.map.t[idx(x, y)].walkable() && k != Kind::Floor => 'r',
_ if g.map.t[idx(x, y)].walkable() => '.', _ 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); assert_eq!(g.map.t[idx(m, BRIDGE_Y + 4)].kind, Kind::Water);
// the tongue connects both banks // 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!(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] #[test]
+9
View File
@@ -381,6 +381,15 @@ impl Game {
} }
// armed traps go off when enemies come close // 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) { fn update_traps(&mut self, dt: f32) {
// poison clouds and rolling boulders // poison clouds and rolling boulders
for c in self.clouds.iter_mut() { for c in self.clouds.iter_mut() {