- New room: aerie (key Y, rooms category, campaign from mission 7); flyers sleep there - Arrivals: aerie 4+ bats; + torture chamber gargoyles; + library owls; big aerie and treasury (16+ each) a rare ember wyrm; portal checks free places in lairs and aerie separately - Flyers cross water and lava (path finding, flag, attacks), are not burnt by lava, patrol far when idle, attack single enemies, flee to the aerie below 25% health and rest there up to 75% - Owls shoot arcane bolts, the wyrm breathes fire with splash damage; shots from the air - Hovering sprites with flapping, aerie floor, perches, roosts, plinths, twig and ember nests - Room counts are recounted on load (no longer stored); tests for flying and arrivals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
526 lines
21 KiB
Rust
526 lines
21 KiB
Rust
// Campaign "The Broken Core": 20 missions. Each mission is a set of key data (size, rivers,
|
|
// fortresses, goals, available tools); the map generator builds the map from it.
|
|
// Texts (title, story, hints) come from the language files: camp.<key>.title / .story
|
|
|
|
use crate::fortress::FortSpec;
|
|
use crate::game::*;
|
|
use crate::map::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
|
|
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
|
|
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie
|
|
pub const TOOL_UNLOCK: [usize; 19] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1, 6];
|
|
|
|
pub struct FortDef {
|
|
pub at: (f32, f32), // centre as a fraction of the map size
|
|
pub size: (i32, i32),
|
|
pub doors: &'static [usize],
|
|
pub towers: &'static [TowerKind],
|
|
pub tower_level: u32,
|
|
pub guards: u32, // number of guards
|
|
pub patrols: u32, // patrol groups of two
|
|
pub level: u32, // level of the knights
|
|
pub boss: Option<(EK, u32)>,
|
|
pub vaults: u32,
|
|
pub vault_value: i32,
|
|
pub shard: bool,
|
|
pub heart: bool,
|
|
}
|
|
|
|
pub struct MissionDef {
|
|
pub key: &'static str,
|
|
pub size: (usize, usize),
|
|
pub seed: u32,
|
|
pub gold: f32,
|
|
pub portals: usize,
|
|
pub treasures: usize,
|
|
pub goals: Vec<Goal>,
|
|
pub waves: bool, // hero waves (from the gate)
|
|
pub wave_limit: u32, // 0 = endless
|
|
pub first_wave: f32, // seconds until the first wave
|
|
pub gate: bool, // free-game hero gate at the far side (wave missions)
|
|
pub gate_defenders: bool,
|
|
pub time_limit: f32,
|
|
pub rivers: Vec<River>,
|
|
pub forts: Vec<FortDef>,
|
|
pub gift: Vec<(EK, u32)>, // creatures that join at the start
|
|
}
|
|
|
|
// carried over from the last won mission: the strongest creatures and part of the gold
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
pub struct Carry {
|
|
pub creatures: Vec<(EK, u32, f32)>, // kind, level, experience
|
|
pub gold: f32,
|
|
}
|
|
|
|
pub const CARRY_CREATURES: usize = 5;
|
|
pub const CARRY_GOLD: f32 = 0.2;
|
|
|
|
// progress of the campaign (stored in the browser)
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
pub struct Progress {
|
|
pub done: Vec<bool>,
|
|
pub carry: Vec<Option<Carry>>, // carry[i] = taken from mission i into mission i + 1
|
|
}
|
|
|
|
impl Progress {
|
|
pub fn unlocked(&self, i: usize) -> bool {
|
|
i == 0 || self.done.get(i - 1).copied().unwrap_or(false) || self.done.get(i).copied().unwrap_or(false)
|
|
}
|
|
pub fn is_done(&self, i: usize) -> bool {
|
|
self.done.get(i).copied().unwrap_or(false)
|
|
}
|
|
pub fn set_done(&mut self, i: usize, carry: Carry) {
|
|
let n = missions().len();
|
|
self.done.resize(n, false);
|
|
self.carry.resize(n, None);
|
|
self.done[i] = true;
|
|
self.carry[i] = Some(carry);
|
|
}
|
|
// what the player brings into mission i
|
|
pub fn carry_into(&self, i: usize) -> Option<&Carry> {
|
|
if i == 0 {
|
|
return None;
|
|
}
|
|
self.carry.get(i - 1).and_then(|c| c.as_ref())
|
|
}
|
|
}
|
|
|
|
use EK::*;
|
|
use TowerKind::{Arrow, Cannon, Magic};
|
|
|
|
fn river(lava: bool, vertical: bool, pos: f32, width: i32, bridges: u32) -> River {
|
|
River { lava, vertical, pos, width, bridges }
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn fort(at: (f32, f32), size: (i32, i32), doors: &'static [usize], towers: &'static [TowerKind], tower_level: u32, guards: u32, patrols: u32, level: u32, vaults: u32, vault_value: i32) -> FortDef {
|
|
FortDef { at, size, doors, towers, tower_level, guards, patrols, level, boss: None, vaults, vault_value, shard: false, heart: false }
|
|
}
|
|
|
|
impl FortDef {
|
|
fn shard(mut self) -> FortDef {
|
|
self.shard = true;
|
|
self
|
|
}
|
|
fn heart(mut self) -> FortDef {
|
|
self.heart = true;
|
|
self
|
|
}
|
|
fn boss(mut self, k: EK, level: u32) -> FortDef {
|
|
self.boss = Some((k, level));
|
|
self
|
|
}
|
|
}
|
|
|
|
fn base(key: &'static str, size: (usize, usize), seed: u32, goals: Vec<Goal>) -> MissionDef {
|
|
MissionDef {
|
|
key,
|
|
size,
|
|
seed,
|
|
gold: 3000.0,
|
|
portals: 1,
|
|
treasures: 0,
|
|
goals,
|
|
waves: false,
|
|
wave_limit: 0,
|
|
first_wave: 300.0,
|
|
gate: false,
|
|
gate_defenders: false,
|
|
time_limit: 0.0,
|
|
rivers: Vec::new(),
|
|
forts: Vec::new(),
|
|
gift: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn missions() -> Vec<MissionDef> {
|
|
let mut v = Vec::new();
|
|
// ---- Act I: awakening
|
|
v.push(MissionDef { gold: 2500.0, forts: vec![fort((0.74, 0.5), (15, 13), &[3], &[], 1, 4, 0, 1, 1, 1200).shard()], ..base("m01", (64, 64), 101, vec![Goal::Shards]) });
|
|
v.push(MissionDef {
|
|
gold: 2500.0,
|
|
rivers: vec![river(false, true, 0.47, 3, 0)],
|
|
forts: vec![fort((0.8, 0.5), (15, 15), &[3], &[Arrow], 1, 4, 1, 1, 1, 1500).shard()],
|
|
..base("m02", (80, 64), 202, vec![Goal::Shards])
|
|
});
|
|
v.push(MissionDef { forts: vec![fort((0.74, 0.5), (19, 17), &[3, 0], &[Arrow, Arrow], 1, 6, 2, 1, 1, 1500)], ..base("m03", (80, 80), 303, vec![Goal::KillAll]) });
|
|
v.push(MissionDef {
|
|
portals: 2,
|
|
treasures: 1,
|
|
waves: true,
|
|
wave_limit: 4,
|
|
first_wave: 270.0,
|
|
gate: true,
|
|
..base("m04", (80, 64), 404, vec![Goal::Survive(4)])
|
|
});
|
|
v.push(MissionDef {
|
|
rivers: vec![river(false, true, 0.42, 3, 1)],
|
|
forts: vec![fort((0.76, 0.5), (21, 21), &[3, 2], &[Arrow, Arrow, Arrow], 1, 6, 2, 2, 4, 2000)],
|
|
..base("m05", (96, 80), 505, vec![Goal::Loot(6000)])
|
|
});
|
|
// ---- Act II: the rivers
|
|
v.push(MissionDef {
|
|
rivers: vec![river(false, true, 0.52, 5, 1)],
|
|
forts: vec![fort((0.82, 0.5), (21, 19), &[3], &[Arrow, Cannon, Arrow, Cannon], 1, 8, 2, 2, 2, 1500).heart()],
|
|
..base("m06", (96, 80), 606, vec![Goal::Heart])
|
|
});
|
|
v.push(MissionDef {
|
|
rivers: vec![river(true, true, 0.45, 3, 0)],
|
|
forts: vec![fort((0.79, 0.45), (19, 19), &[3], &[Arrow, Cannon], 2, 7, 2, 2, 2, 1800).shard()],
|
|
..base("m07", (96, 96), 707, vec![Goal::Shards])
|
|
});
|
|
v.push(MissionDef {
|
|
time_limit: 900.0,
|
|
gift: vec![(Ogre, 3), (Kobold, 3), (Kobold, 3), (Spider, 2)],
|
|
forts: vec![
|
|
fort((0.6, 0.27), (17, 15), &[3, 2], &[Arrow], 1, 4, 1, 2, 3, 1500),
|
|
fort((0.78, 0.72), (17, 15), &[3, 0], &[Arrow, Cannon], 1, 4, 1, 2, 3, 1500),
|
|
],
|
|
..base("m08", (96, 80), 808, vec![Goal::Loot(8000)])
|
|
});
|
|
v.push(MissionDef {
|
|
rivers: vec![river(false, false, 0.5, 3, 2)],
|
|
forts: vec![
|
|
fort((0.76, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(),
|
|
fort((0.76, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(),
|
|
],
|
|
..base("m09", (112, 96), 909, vec![Goal::Shards])
|
|
});
|
|
v.push(MissionDef {
|
|
gold: 4000.0,
|
|
portals: 2,
|
|
treasures: 1,
|
|
waves: true,
|
|
wave_limit: 6,
|
|
first_wave: 240.0,
|
|
gate: true,
|
|
..base("m10", (96, 80), 1010, vec![Goal::Survive(6)])
|
|
});
|
|
v.push(MissionDef {
|
|
forts: vec![fort((0.72, 0.5), (25, 25), &[3, 0, 2], &[Arrow, Cannon, Magic, Arrow, Cannon], 2, 8, 4, 3, 2, 2000)],
|
|
..base("m11", (112, 96), 1111, vec![Goal::KillAll])
|
|
});
|
|
v.push(MissionDef {
|
|
rivers: vec![river(false, true, 0.4, 3, 1), river(true, true, 0.62, 3, 1)],
|
|
forts: vec![fort((0.85, 0.5), (21, 25), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart().shard()],
|
|
..base("m12", (112, 96), 1212, vec![Goal::Heart, Goal::Shards])
|
|
});
|
|
// ---- Act III: the citadel
|
|
v.push(MissionDef {
|
|
rivers: vec![river(false, false, 0.22, 3, 1)],
|
|
forts: vec![fort((0.75, 0.55), (27, 23), &[3, 0], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 2, 12, 3, 3, 4, 3500)],
|
|
..base("m13", (112, 96), 1313, vec![Goal::Loot(13000)])
|
|
});
|
|
v.push(MissionDef {
|
|
forts: vec![fort((0.7, 0.5), (25, 25), &[3, 1], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Cannon], 3, 8, 2, 3, 2, 2000)],
|
|
..base("m14", (112, 112), 1414, vec![Goal::Towers])
|
|
});
|
|
v.push(MissionDef {
|
|
portals: 0,
|
|
gold: 5000.0,
|
|
gift: vec![(Ogre, 5), (Ogre, 4), (Mage, 5), (Mage, 4), (Spider, 4), (Kobold, 4), (Kobold, 4), (Mage, 3)],
|
|
rivers: vec![river(false, true, 0.42, 4, 1)],
|
|
forts: vec![fort((0.74, 0.5), (21, 21), &[3, 0, 2], &[Arrow, Cannon, Magic], 2, 10, 2, 3, 2, 2000)],
|
|
..base("m15", (112, 96), 1515, vec![Goal::KillAll])
|
|
});
|
|
v.push(MissionDef {
|
|
waves: true,
|
|
first_wave: 360.0,
|
|
gate_defenders: true,
|
|
rivers: vec![river(true, true, 0.55, 3, 2)],
|
|
forts: vec![fort((0.82, 0.5), (23, 23), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart()],
|
|
..base("m16", (112, 96), 1616, vec![Goal::Heart])
|
|
});
|
|
v.push(MissionDef {
|
|
portals: 2,
|
|
rivers: vec![river(false, true, 0.36, 3, 2), river(false, true, 0.56, 3, 2), river(false, false, 0.5, 3, 2)],
|
|
forts: vec![
|
|
fort((0.8, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(),
|
|
fort((0.8, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(),
|
|
],
|
|
..base("m17", (128, 112), 1717, vec![Goal::Shards, Goal::Loot(6000)])
|
|
});
|
|
v.push(MissionDef {
|
|
portals: 2,
|
|
rivers: vec![river(true, true, 0.5, 4, 1)],
|
|
forts: vec![fort((0.82, 0.5), (25, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 3, 14, 3, 4, 3, 2500).heart().boss(Paladin, 5)],
|
|
..base("m18", (128, 112), 1818, vec![Goal::Heart])
|
|
});
|
|
v.push(MissionDef {
|
|
portals: 2,
|
|
rivers: vec![river(false, true, 0.42, 3, 2), river(true, false, 0.5, 3, 2)],
|
|
forts: vec![
|
|
fort((0.62, 0.22), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(),
|
|
fort((0.86, 0.5), (19, 19), &[3], &[Arrow, Cannon, Magic, Arrow], 3, 10, 2, 4, 1, 2500).shard(),
|
|
fort((0.62, 0.8), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(),
|
|
],
|
|
..base("m19", (144, 128), 1919, vec![Goal::Shards])
|
|
});
|
|
v.push(MissionDef {
|
|
portals: 3,
|
|
gold: 5000.0,
|
|
waves: true,
|
|
first_wave: 420.0,
|
|
gate_defenders: true,
|
|
rivers: vec![river(false, true, 0.6, 6, 2)],
|
|
forts: vec![fort((0.83, 0.5), (29, 29), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Magic], 3, 16, 4, 5, 4, 3000)
|
|
.heart()
|
|
.shard()
|
|
.boss(Paladin, 10)],
|
|
..base("m20", (144, 144), 2020, vec![Goal::Heart, Goal::Shards])
|
|
});
|
|
v
|
|
}
|
|
|
|
// the knights of a fortress: mostly knights and archers, some priests and dwarves
|
|
fn knights(n: u32, level: u32) -> Vec<(EK, u32)> {
|
|
let mix = [Knight, Archer, Knight, Priest, Dwarf, Archer, Knight];
|
|
(0..n).map(|k| (mix[k as usize % mix.len()], level)).collect()
|
|
}
|
|
|
|
impl FortDef {
|
|
fn spec(&self, w: usize, h: usize) -> FortSpec {
|
|
let (fw, fh) = self.size;
|
|
let x = ((self.at.0 * w as f32) as i32 - fw / 2).clamp(3, w as i32 - fw - 3);
|
|
let y = ((self.at.1 * h as f32) as i32 - fh / 2).clamp(3, h as i32 - fh - 3);
|
|
let mut guards = knights(self.guards, self.level);
|
|
if let Some(b) = self.boss {
|
|
guards.insert(0, b);
|
|
}
|
|
let patrols = (0..self.patrols).map(|k| vec![(if k % 2 == 0 { Knight } else { Archer }, self.level), (Knight, self.level)]).collect();
|
|
FortSpec {
|
|
x,
|
|
y,
|
|
w: fw,
|
|
h: fh,
|
|
doors: self.doors.to_vec(),
|
|
towers: self.towers.to_vec(),
|
|
tower_level: self.tower_level,
|
|
guards,
|
|
patrols,
|
|
vaults: self.vaults,
|
|
vault_value: self.vault_value,
|
|
shard: self.shard,
|
|
heart: self.heart,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Builds the game of mission i (carry: what the last mission handed over)
|
|
pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
|
let all = missions();
|
|
let d = &all[i.min(all.len() - 1)];
|
|
let (w, h) = d.size;
|
|
let opts = GenOpts { portals: Some(d.portals), treasures: Some(d.treasures), gate: d.gate, core_y: None };
|
|
let map = Map::generate_with(d.seed, w, h, &opts);
|
|
let mut g = Game::from_map(map, d.seed);
|
|
g.gold = d.gold;
|
|
for (k, r) in d.rivers.iter().enumerate() {
|
|
let seed = d.seed.wrapping_mul(31).wrapping_add(k as u32 * 977);
|
|
g.map.carve_river(r, seed);
|
|
}
|
|
let mut shards = 0;
|
|
for f in &d.forts {
|
|
g.build_fortress(&f.spec(w, h));
|
|
shards += f.shard as u32;
|
|
}
|
|
let heart = d.gate || d.forts.iter().any(|f| f.heart);
|
|
if !heart {
|
|
g.gate_hp = 0.0;
|
|
}
|
|
g.guard_treasures();
|
|
g.place_all_torches();
|
|
g.mission = Some(MissionState {
|
|
index: i,
|
|
goals: d.goals.clone(),
|
|
tools: TOOL_UNLOCK.iter().map(|&u| u <= i).collect(),
|
|
waves: d.waves,
|
|
wave_limit: d.wave_limit,
|
|
gate_defenders: d.gate_defenders,
|
|
heart,
|
|
time_limit: d.time_limit,
|
|
shards,
|
|
});
|
|
g.wave_timer = d.first_wave;
|
|
// new creatures stand around the core
|
|
let (cx, cy) = g.map.core;
|
|
let mut joiners: Vec<(EK, u32, f32)> = d.gift.iter().map(|&(k, l)| (k, l, 0.0)).collect();
|
|
if let Some(c) = carry {
|
|
joiners.extend(c.creatures.iter().copied());
|
|
g.gold += c.gold;
|
|
}
|
|
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
|
|
let a = k as f32 * 0.9;
|
|
let (x, y) = (cx as f32 + 1.5 + a.cos() * 3.2, cy as f32 + 1.5 + a.sin() * 3.2);
|
|
let id = g.spawn(kind, false, x, y, level.max(1));
|
|
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
|
e.xp = xp;
|
|
e.happy = 1.0;
|
|
}
|
|
}
|
|
g.msg("msg.mission_start", vec![format!("@camp.{}.title", d.key)]);
|
|
g
|
|
}
|
|
|
|
// the strongest creatures and a fifth of the gold go along into the next mission
|
|
pub fn carry_from(g: &Game) -> Carry {
|
|
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
|
let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval;
|
|
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal));
|
|
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor() }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn all_missions_build() {
|
|
for (i, d) in missions().iter().enumerate() {
|
|
let g = start_mission(i, None);
|
|
let m = g.mission.as_ref().unwrap();
|
|
let heroes = g.ents.iter().filter(|e| e.hero).count();
|
|
println!("{} {}x{}: {} knights, {} knight towers, {} shards, goals {:?}", d.key, d.size.0, d.size.1, heroes, g.towers.iter().filter(|t| t.hero).count(), m.shards, m.goals);
|
|
// every patrol group got its round
|
|
let patrols = g.ents.iter().filter(|e| !e.patrol.is_empty()).count() as u32;
|
|
assert_eq!(patrols, d.forts.iter().map(|f| f.patrols * 2).sum::<u32>(), "{}", d.key);
|
|
// the core area is intact
|
|
let (cx, cy) = g.map.core;
|
|
assert_eq!(g.map.t[idx(cx + 1, cy + 1)].kind, Kind::Core, "{}", d.key);
|
|
// goals that need something on the map have it
|
|
for goal in &m.goals {
|
|
match goal {
|
|
Goal::Shards => assert!(m.shards > 0, "{}", d.key),
|
|
Goal::Heart => assert!(m.heart && g.gate_hp > 0.0, "{}", d.key),
|
|
Goal::Towers => assert!(g.towers.iter().any(|t| t.hero), "{}", d.key),
|
|
Goal::KillAll => assert!(heroes > 0, "{}", d.key),
|
|
Goal::Loot(n) => {
|
|
let vaults: i32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Hero).map(|t| t.gold).sum();
|
|
assert!(vaults as u32 >= *n, "{}: vaults hold {}", d.key, vaults);
|
|
}
|
|
Goal::Survive(_) => assert!(m.waves, "{}", d.key),
|
|
}
|
|
}
|
|
// wave missions: the heroes can reach the core
|
|
if m.waves {
|
|
let (gx, gy) = g.map.gate;
|
|
assert!(g.map.hero_path_to_core(idx(gx + 1, gy + 1)).is_some(), "{}: no hero path", d.key);
|
|
}
|
|
// every shard and the heart can be reached by digging and bridging from the core
|
|
let reach = g.map.search(idx(cx + 1, cy - 2).max(0), |i| {
|
|
let t = &g.map.t[i];
|
|
if t.kind == Kind::Rock || t.kind == Kind::Core || t.kind == Kind::Tower {
|
|
None
|
|
} else {
|
|
Some(1)
|
|
}
|
|
}, |i| g.map.t[i].kind == Kind::Shard || g.map.t[i].kind == Kind::Gate || g.map.t[i].owner == Owner::Hero, true);
|
|
assert!(reach.is_some() || heroes == 0, "{}: fortress unreachable", d.key);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missions_run_and_are_not_decided_at_once() {
|
|
for (i, d) in missions().iter().enumerate() {
|
|
let mut g = start_mission(i, None);
|
|
let m = g.mission.clone().unwrap();
|
|
assert!(!m.goals.iter().all(|&goal| g.goal_done(goal)), "{}: goals done at the start", d.key);
|
|
let t = std::time::Instant::now();
|
|
for _ in 0..(60 * 20) {
|
|
g.update(0.05);
|
|
}
|
|
println!("{}: 60 s simulated in {} ms, state {}, core {:.0}", d.key, t.elapsed().as_millis(), g.state, g.core_hp);
|
|
assert_eq!(g.state, 0, "{}: decided without the player", d.key);
|
|
}
|
|
}
|
|
|
|
// A simple scripted player: digs a corridor to the shard, bridges rivers on the way and
|
|
// sends a few strong creatures along with the flag.
|
|
fn play_to_shard(i: usize) -> (u8, f32) {
|
|
let mut g = start_mission(i, None);
|
|
let (cx, cy) = g.map.core;
|
|
let shard = g.map.t.iter().position(|t| t.kind == Kind::Shard).unwrap();
|
|
let path = g
|
|
.map
|
|
.search(idx(cx + 1, cy - 2), |j| {
|
|
let t = &g.map.t[j];
|
|
match t.kind {
|
|
Kind::Rock | Kind::Core | Kind::Tower => None,
|
|
Kind::Water | Kind::Lava => Some(3),
|
|
k if t.solid() => Some(if t.reinforced { 6 } else { 2 }),
|
|
_ => Some(1),
|
|
}
|
|
}, |j| j == shard, false)
|
|
.expect("a way to the shard");
|
|
for &j in &path {
|
|
if g.map.t[j].diggable() {
|
|
g.map.t[j].marked = true;
|
|
}
|
|
}
|
|
for k in [EK::Ogre, EK::Ogre, EK::Mage, EK::Ogre] {
|
|
g.spawn(k, false, cx as f32 + 4.5, cy as f32 + 1.5, 4);
|
|
}
|
|
g.gold = 3000.0;
|
|
// lair and mushroom garden at the sides of the start area
|
|
g.build_rect(1, cx - 3, cy - 3, cx - 2, cy + 5);
|
|
g.build_rect(2, cx + 4, cy - 3, cx + 5, cy + 5);
|
|
g.gold = 2500.0; // as if the player had mined some gold
|
|
let mut t = 0.0;
|
|
while t < 1500.0 && g.state == 0 {
|
|
for _ in 0..20 {
|
|
g.update(0.05);
|
|
}
|
|
t += 1.0;
|
|
// fallen imps are replaced
|
|
if t as i32 % 10 == 0 && g.worker_count() < 4 {
|
|
g.summon(cx as f32 + 1.5, cy as f32 - 1.5);
|
|
}
|
|
// bridges: every few seconds try to bridge the water on the path
|
|
if t as i32 % 5 == 0 {
|
|
for &j in &path {
|
|
if g.map.t[j].liquid() {
|
|
let (x, y) = xy(j);
|
|
g.build_bridges(x, y, x, y);
|
|
}
|
|
}
|
|
// the creatures follow the digging: flag at the furthest open tile of the path
|
|
if let Some(&j) = path.iter().rev().find(|&&j| g.map.t[j].walkable()) {
|
|
let (x, y) = xy(j);
|
|
g.set_flag(x, y);
|
|
}
|
|
}
|
|
}
|
|
(g.state, t)
|
|
}
|
|
|
|
#[test]
|
|
fn first_missions_can_be_won() {
|
|
for i in [0, 1] {
|
|
let (state, t) = play_to_shard(i);
|
|
println!("mission {}: state {} after {} s", i + 1, state, t);
|
|
assert_eq!(state, 1, "mission {} not won", i + 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn carry_over_takes_the_strongest() {
|
|
let mut g = start_mission(0, None);
|
|
for (k, l) in [(Kobold, 1), (Ogre, 5), (Spider, 2), (Mage, 4), (Kobold, 2), (Ogre, 2), (Spider, 1)] {
|
|
g.spawn(k, false, 20.0, 20.0, l);
|
|
}
|
|
g.gold = 5000.0;
|
|
let c = carry_from(&g);
|
|
assert_eq!(c.creatures.len(), CARRY_CREATURES);
|
|
assert_eq!(c.creatures[0].0, Ogre);
|
|
assert_eq!(c.gold, 1000.0);
|
|
let g2 = start_mission(1, Some(&c));
|
|
assert_eq!(g2.ents.iter().filter(|e| e.creature()).count(), CARRY_CREATURES);
|
|
assert_eq!(g2.gold, 2500.0 + 1000.0);
|
|
// the tools of mission 2: bridges yes, training no
|
|
assert!(g2.tool_allowed(17));
|
|
assert!(!g2.tool_allowed(5));
|
|
}
|
|
}
|