// 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..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, 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, pub forts: Vec, 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, pub carry: Vec>, // 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) -> 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 { 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::(), "{}", 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)); } }