581 lines
24 KiB
Rust
581 lines
24 KiB
Rust
// 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 their gate stands above
|
|
// ground in a sunny forest east of a cliff, from where they march in through two cave mouths. 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::*;
|
|
use crate::render3d::Cam;
|
|
|
|
const W: usize = 72;
|
|
const H: usize = 48;
|
|
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
|
|
|
|
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 WALL_X: i32 = RIVER_X - 8; // the dungeon's wall with its three doors
|
|
const SURF_X: i32 = 55; // from here east the land lies above ground (the knights come from there)
|
|
|
|
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();
|
|
let mut r = rng.next() % total;
|
|
for &(k, w) in pool {
|
|
if r < w {
|
|
return k;
|
|
}
|
|
r -= w;
|
|
}
|
|
pool[0].0
|
|
}
|
|
|
|
impl Game {
|
|
pub fn new_demo(seed: u32) -> (Game, Cam) {
|
|
let opts = GenOpts { portals: Some(0), treasures: Some(0), gate: false, core_y: Some(H as i32 / 2 - 1) };
|
|
let map = Map::generate_with(seed, W, H, &opts);
|
|
let mut g = Game::from_map(map, seed);
|
|
g.ents.clear();
|
|
let mut rng = Rng(seed | 1);
|
|
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.reinforced = false;
|
|
t.owner = if x < m - 2 { Owner::Player } else if x > m + 2 { Owner::Hero } else { Owner::None };
|
|
}
|
|
}
|
|
// 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.reinforced = false;
|
|
t.kind = if y < BRIDGE_Y { Kind::Lava } else if y >= BRIDGE_Y + 3 { Kind::Water } else { Kind::Floor };
|
|
}
|
|
}
|
|
// 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;
|
|
}
|
|
// 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.kind = if rock { Kind::Rock } else { Kind::Earth };
|
|
t.owner = Owner::None;
|
|
t.reinforced = !rock && x < RIVER_X - 6;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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)];
|
|
if t.kind == Kind::Floor && t.owner == Owner::Player {
|
|
t.kind = k;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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_mid(n) - 4, n, TowerKind::Arrow, false, 0.0),
|
|
(river_mid(s_) - 4, s_, TowerKind::Cannon, 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 + 8, BRIDGE_Y - 7, TowerKind::Cannon, true, 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;
|
|
// above ground in the east: grass, a forest at the edges, a cliff with two cave mouths
|
|
let mouth = |y: i32| (BRIDGE_Y - 4..=BRIDGE_Y - 1).contains(&y) || (BRIDGE_Y + 2..=BRIDGE_Y + 6).contains(&y);
|
|
for y in 1..H as i32 - 1 {
|
|
for x in SURF_X - 1..W as i32 - 1 {
|
|
let i = idx(x, y);
|
|
let border = y < 2 || y >= H as i32 - 2 || x >= W as i32 - 2;
|
|
if x < SURF_X + 1 {
|
|
// the cliff (or a cave mouth: knight ground)
|
|
if !border {
|
|
let t = &mut g.map.t[i];
|
|
t.kind = if mouth(y) { Kind::Floor } else { Kind::Rock };
|
|
t.owner = if mouth(y) { Owner::Hero } else { Owner::None };
|
|
t.reinforced = false;
|
|
t.gold = 0;
|
|
}
|
|
continue;
|
|
}
|
|
g.map.set_sky(i, true);
|
|
if border {
|
|
continue;
|
|
}
|
|
let t = &mut g.map.t[i];
|
|
t.owner = Owner::None;
|
|
t.gold = 0;
|
|
t.reinforced = false;
|
|
if t.kind == Kind::Rock && rng.f() < 0.6 {
|
|
continue; // a few rocks stay as outcrops
|
|
}
|
|
// trees along the north and south edges and in the far east; the march route stays open
|
|
let dy = (y - (BRIDGE_Y + 1)).abs();
|
|
let h = ((x * 73 + y * 151) as u32).wrapping_mul(2654435761) >> 24;
|
|
let wood = dy > 9 || x > W as i32 - 7;
|
|
t.kind = if wood && h % 3 != 0 { Kind::Tree } else { Kind::Floor };
|
|
t.hp = if t.kind == Kind::Tree { crate::wood::TREE_HP } else { 0.0 };
|
|
}
|
|
}
|
|
// the knight cave runs on to the cave mouths
|
|
for y in BRIDGE_Y - 4..=BRIDGE_Y + 6 {
|
|
if !mouth(y) {
|
|
continue;
|
|
}
|
|
let mut x = SURF_X - 1;
|
|
while x > RIVER_X + 3 && !g.map.t[idx(x, y)].walkable() {
|
|
let t = &mut g.map.t[idx(x, y)];
|
|
t.kind = Kind::Floor;
|
|
t.owner = Owner::Hero;
|
|
t.reinforced = false;
|
|
x -= 1;
|
|
}
|
|
}
|
|
// the hero gate on the knights' side, in the forest
|
|
let (gx, gy) = (SURF_X + 6, 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();
|
|
g.place_all_torches();
|
|
// start with a fight already going on
|
|
for _ in 0..6 {
|
|
g.demo_spawn(true);
|
|
g.demo_spawn(false);
|
|
}
|
|
// the camera between the battle on the tongue and the forest the knights come from
|
|
let mut cam = Cam::new(RIVER_X as f32 + 11.5, BRIDGE_Y as f32 + 1.5);
|
|
cam.zoom = 21.0;
|
|
(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();
|
|
for (x, y, kind, hero) in slots {
|
|
self.demo_tower(x, y, kind, hero);
|
|
}
|
|
}
|
|
|
|
fn demo_tower(&mut self, x: i32, y: i32, kind: TowerKind, hero: bool) {
|
|
let i = idx(x, y);
|
|
if self.towers.iter().any(|t| t.tile == i) {
|
|
return;
|
|
}
|
|
let s = tower_stats(kind);
|
|
let t = &mut self.map.t[i];
|
|
t.kind = Kind::Tower;
|
|
t.owner = if hero { Owner::Hero } else { Owner::Player };
|
|
let id = self.next_id;
|
|
self.next_id += 1;
|
|
let level = 1 + (self.demo.as_mut().map_or(0, |d| d.rng.next() % 2));
|
|
let mut tw = Tower { id, kind, tile: i, level, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero, rune_final: false };
|
|
tw.hp = tw.max_hp();
|
|
self.towers.push(tw);
|
|
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff);
|
|
}
|
|
|
|
// a new unit at the far end of its side
|
|
fn demo_spawn(&mut self, creature: bool) {
|
|
let Some(d) = self.demo.as_mut() else { return };
|
|
let kind = pick(&mut d.rng, if creature { &CREATURES } else { &KNIGHTS });
|
|
let level = 2 + d.rng.next() % 6;
|
|
let y = BRIDGE_Y as f32 + 1.5 + (d.rng.f() - 0.5) * 14.0;
|
|
let x = if creature { 9.0 + d.rng.f() * 6.0 } else { W as f32 - 15.0 + d.rng.f() * 6.0 };
|
|
if !self.map.walk(x as i32, y as i32) {
|
|
return;
|
|
}
|
|
let id = self.spawn(kind, !creature, x, y, level);
|
|
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
|
e.happy = 1.0;
|
|
e.settled = true;
|
|
}
|
|
}
|
|
|
|
// the demo instead of the normal game: units, towers, reinforcements, rebuilding
|
|
pub fn demo_step(&mut self, dt: f32) {
|
|
self.time += dt;
|
|
self.core_hp = CORE_HP;
|
|
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;
|
|
if d.spawn_t <= 0.0 {
|
|
d.spawn_t = 1.0;
|
|
let side = |hero: bool| -> (usize, f32) {
|
|
let list: Vec<(EK, u32)> = self.ents.iter().filter(|e| e.alive && e.hero == hero && e.captive == 0).map(|e| (e.kind, e.level)).collect();
|
|
(list.len(), Game::squad_value(&list))
|
|
};
|
|
let (nc, vc) = side(false);
|
|
let (nk, vk) = side(true);
|
|
if nc < PER_SIDE && (vc <= vk || nk >= PER_SIDE) {
|
|
want = Some(true);
|
|
} else if nk < PER_SIDE {
|
|
want = Some(false);
|
|
}
|
|
}
|
|
// destroyed towers come back after a while
|
|
let mut rebuild = Vec::new();
|
|
for s in d.towers.iter_mut() {
|
|
let i = idx(s.0, s.1);
|
|
if self.towers.iter().any(|t| t.tile == i) {
|
|
s.4 = 0.0;
|
|
} else {
|
|
s.4 += dt;
|
|
if s.4 >= REBUILD {
|
|
s.4 = 0.0;
|
|
rebuild.push((s.0, s.1, s.2, s.3));
|
|
}
|
|
}
|
|
}
|
|
self.demo = Some(d);
|
|
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));
|
|
if !blocked {
|
|
self.demo_tower(x, y, kind, hero);
|
|
}
|
|
}
|
|
// the battle never ends
|
|
self.state = 0;
|
|
self.msgs.clear();
|
|
}
|
|
|
|
// demo units: fight what is close, tear down enemy towers, otherwise push over the bridge
|
|
pub fn demo_ai(&mut self, e: &mut Ent, dt: f32) {
|
|
if e.captive != 0 {
|
|
e.alive = false;
|
|
return;
|
|
}
|
|
if e.think <= 0.0 {
|
|
e.think = 0.5;
|
|
// the heavy ones go for the towers first
|
|
let breaker = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Dwarf | EK::Gargoyle);
|
|
if !matches!(e.job, Job::Attack(_) | Job::AttackTower(_)) {
|
|
let enemy = if breaker { None } else { self.nearest_enemy(e, 7.0) };
|
|
if let Some(id) = enemy {
|
|
e.job = Job::Attack(id);
|
|
e.path.clear();
|
|
e.repath = 0.0;
|
|
} else {
|
|
let (x, y) = (e.x, e.y);
|
|
let tower = self.towers.iter().filter(|t| t.hero != e.hero).min_by(|a, b| {
|
|
let da = (a.pos().0 - x).powi(2) + (a.pos().1 - y).powi(2);
|
|
let db = (b.pos().0 - x).powi(2) + (b.pos().1 - y).powi(2);
|
|
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
|
|
});
|
|
if let Some(t) = tower {
|
|
let (tx, ty) = t.pos();
|
|
if (tx - x).powi(2) + (ty - y).powi(2) < 8.0 * 8.0 {
|
|
e.job = Job::AttackTower(t.id);
|
|
e.path.clear();
|
|
e.repath = 0.0;
|
|
}
|
|
}
|
|
if breaker && e.job == Job::None {
|
|
if let Some(id) = self.nearest_enemy(e, 7.0) {
|
|
e.job = Job::Attack(id);
|
|
e.path.clear();
|
|
e.repath = 0.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match e.job {
|
|
Job::Attack(id) => {
|
|
if self.attack_job(e, id, dt) {
|
|
return;
|
|
}
|
|
e.job = Job::None;
|
|
}
|
|
Job::AttackTower(id) => {
|
|
if self.attack_tower(e, id, dt) {
|
|
return;
|
|
}
|
|
e.job = Job::None;
|
|
}
|
|
_ => {}
|
|
}
|
|
// march towards the far bank
|
|
if e.path.is_empty() && e.repath <= 0.0 {
|
|
e.repath = 2.0;
|
|
let d = self.demo.as_mut();
|
|
let r = d.map_or(0.5, |d| d.rng.f());
|
|
let tx = if e.hero { 12 + (r * 8.0) as i32 } else { W as i32 - 20 + (r * 8.0) as i32 };
|
|
let ty = BRIDGE_Y + 1 + ((r * 97.0).fract() * 12.0) as i32 - 6;
|
|
if self.map.walk(tx, ty) {
|
|
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), idx(tx, ty)) } else { self.map.path_to(e.tile(), idx(tx, ty)) };
|
|
if let Some(p) = p {
|
|
e.path = p;
|
|
}
|
|
}
|
|
}
|
|
self.step_path(e, dt);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
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',
|
|
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() => '.',
|
|
_ => ' ',
|
|
})
|
|
.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(), 7);
|
|
assert!(g.traps.len() >= 10, "doors and every trap");
|
|
}
|
|
|
|
#[test]
|
|
fn the_battle_goes_on() {
|
|
let (mut g, _) = Game::new_demo(7);
|
|
let mut kills = 0;
|
|
let mut rebuilt = false;
|
|
let mut lost = false;
|
|
for k in 0..(20 * 240) {
|
|
let before = g.ents.len();
|
|
let towers = g.towers.len();
|
|
g.update(0.05);
|
|
kills += before.saturating_sub(g.ents.len());
|
|
lost |= g.towers.len() < 6;
|
|
rebuilt |= lost && g.towers.len() > towers;
|
|
if k % (20 * 30) == 0 {
|
|
let c = g.ents.iter().filter(|e| e.alive && !e.hero).count();
|
|
let h = g.ents.iter().filter(|e| e.alive && e.hero).count();
|
|
println!("{:>3} s: {} creatures, {} knights, {} towers, {} fallen", k / 20, c, h, g.towers.len(), kills);
|
|
}
|
|
}
|
|
assert_eq!(g.state, 0);
|
|
assert!(kills > 10, "they fight: {} fallen", kills);
|
|
assert!(g.ents.iter().any(|e| e.alive && e.hero) && g.ents.iter().any(|e| e.alive && !e.hero), "both sides are still there");
|
|
println!("towers lost {}, rebuilt {}", lost, rebuilt);
|
|
assert!(rebuilt, "lost towers are rebuilt");
|
|
}
|
|
}
|