// 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, steel pub const TOOL_UNLOCK: [usize; 47] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22, 9, 13, 6, 11, 18, 0, 0]; 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: String, 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 pub rivals: Vec<(f32, f32, u8, u8)>, // rival masters: position (fraction of the map), difficulty, side (2 Mossgrim, 3 Lady Nightwell) pub islands: Vec, pub caves: Vec, } // an island in a lake of water or lava, with treasure and/or a mana spring pub struct IslandDef { pub at: (f32, f32), pub r: i32, pub lava: bool, pub spring: bool, pub treasure: i32, } // a sealed cave: treasure and/or a lone creature in chains that joins when the cave is claimed pub struct CaveDef { pub at: (f32, f32), pub chained: Option<(EK, u32)>, pub treasure: i32, } fn island(at: (f32, f32), lava: bool, spring: bool, treasure: i32) -> IslandDef { IslandDef { at, r: 2, lava, spring, treasure } } fn cave(at: (f32, f32), chained: Option<(EK, u32)>, treasure: i32) -> CaveDef { CaveDef { at, chained, treasure } } // 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, #[serde(default)] pub rune_free: u32, // unbuilt rune forge towers from chests #[serde(default)] pub rune_final: u32, // unbuilt or sold rune forge towers that were already given back once } 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: &str, size: (usize, usize), seed: u32, goals: Vec) -> MissionDef { MissionDef { key: key.to_string(), 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(), rivals: Vec::new(), islands: Vec::new(), caves: Vec::new(), } } // Converts one campaign editor mission into the same shape the campaign generator uses. pub fn mission_def_from_doc(d: &crate::campaign_doc::MissionDoc) -> MissionDef { let mut out = base(&d.key, d.size, d.seed, crate::campaign_doc::parse_goals(&d.goals)); out.gold = d.gold; out.portals = d.portals; out.treasures = d.treasures; out.waves = d.waves; out.wave_limit = d.wave_limit; out.first_wave = d.first_wave; out.gate = d.waves; out.gate_defenders = d.waves; out.time_limit = d.time_limit; out } // Rivers are at least four tiles wide: a ranged attack just reaches across. 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, 4, 0)], forts: vec![fort((0.82, 0.5), (15, 15), &[3], &[Arrow], 1, 4, 1, 1, 1, 1500).shard()], ..base("m02", (72, 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, 72), 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, 72), 404, vec![Goal::Survive(4)]) }); v.push(MissionDef { rivals: vec![(0.78, 0.5, 0, 2)], islands: vec![island((0.5, 0.2), false, false, 1200)], ..base("m21", (80, 80), 2121, vec![Goal::Rivals]) }); v.push(MissionDef { rivers: vec![river(false, true, 0.42, 4, 1)], islands: vec![island((0.45, 0.8), false, false, 1500)], forts: vec![fort((0.78, 0.5), (21, 21), &[3, 2], &[Arrow, Arrow, Arrow], 1, 6, 2, 2, 4, 2000)], ..base("m05", (88, 80), 505, vec![Goal::Loot(6000)]) }); v.push(MissionDef { caves: vec![cave((0.5, 0.22), Some((Ogre, 6)), 800), cave((0.56, 0.8), None, 1500)], forts: vec![fort((0.82, 0.5), (17, 17), &[3], &[Arrow, Arrow], 1, 5, 1, 2, 1, 1200).shard()], ..base("m22", (88, 88), 2222, vec![Goal::Freed(1), Goal::Shards]) }); // ---- Act II: the rivers and the other masters v.push(MissionDef { rivers: vec![river(false, true, 0.52, 5, 1)], forts: vec![fort((0.84, 0.5), (21, 19), &[3], &[Arrow, Cannon, Arrow, Cannon], 1, 8, 2, 2, 2, 1500).heart()], ..base("m06", (96, 88), 606, vec![Goal::Heart]) }); v.push(MissionDef { rivers: vec![river(true, true, 0.45, 4, 0)], forts: vec![fort((0.8, 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 { rivers: vec![river(false, true, 0.55, 4, 1)], islands: vec![island((0.4, 0.2), false, false, 2000)], rivals: vec![(0.8, 0.5, 1, 3)], ..base("m23", (104, 96), 2323, vec![Goal::Rivals]) }); 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, 96), 808, vec![Goal::Loot(8000)]) }); v.push(MissionDef { rivers: vec![river(false, false, 0.5, 4, 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", (104, 96), 1010, vec![Goal::Survive(6)]) }); v.push(MissionDef { islands: vec![island((0.42, 0.25), false, true, 0), island((0.6, 0.75), true, true, 800), island((0.74, 0.35), false, true, 1200)], forts: vec![fort((0.86, 0.6), (15, 15), &[3], &[Arrow, Cannon], 2, 6, 2, 2, 1, 1200)], ..base("m24", (112, 104), 2424, vec![Goal::Springs(3)]) }); 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, 104), 1111, vec![Goal::KillAll]) }); v.push(MissionDef { gate: true, waves: true, first_wave: 330.0, rivers: vec![river(false, false, 0.62, 4, 2)], rivals: vec![(0.8, 0.3, 1, 2)], ..base("m25", (120, 104), 2525, vec![Goal::Rivals]) }); v.push(MissionDef { rivers: vec![river(false, true, 0.4, 4, 1), river(true, true, 0.62, 4, 1)], forts: vec![fort((0.86, 0.5), (21, 25), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart().shard()], ..base("m12", (120, 112), 1212, vec![Goal::Heart, Goal::Shards]) }); // ---- Act III: the citadel and the masters v.push(MissionDef { rivers: vec![river(false, false, 0.22, 4, 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", (120, 112), 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", (128, 112), 1414, vec![Goal::Towers]) }); v.push(MissionDef { rivers: vec![river(true, true, 0.62, 4, 1)], islands: vec![island((0.45, 0.78), true, true, 2500)], rivals: vec![(0.84, 0.5, 2, 3)], ..base("m26", (128, 120), 2626, vec![Goal::Rivals]) }); 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", (128, 120), 1515, vec![Goal::KillAll]) }); v.push(MissionDef { waves: true, first_wave: 360.0, gate_defenders: true, rivers: vec![river(true, true, 0.55, 4, 2)], forts: vec![fort((0.84, 0.5), (23, 23), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart()], ..base("m16", (128, 128), 1616, vec![Goal::Heart]) }); v.push(MissionDef { caves: vec![cave((0.42, 0.2), Some((Chainbreaker, 4)), 1000), cave((0.45, 0.82), Some((Gargoyle, 6)), 1000), cave((0.66, 0.5), None, 3000)], islands: vec![island((0.62, 0.2), false, true, 0), island((0.66, 0.82), true, true, 0)], forts: vec![fort((0.86, 0.5), (17, 17), &[3], &[Arrow, Cannon, Magic], 3, 8, 2, 3, 1, 1500)], ..base("m27", (136, 128), 2727, vec![Goal::Freed(2), Goal::Springs(2)]) }); v.push(MissionDef { portals: 2, rivers: vec![river(false, true, 0.36, 4, 2), river(false, true, 0.56, 4, 2), river(false, false, 0.5, 4, 2)], forts: vec![ fort((0.82, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), fort((0.82, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), ], ..base("m17", (136, 136), 1717, vec![Goal::Shards, Goal::Loot(6000)]) }); v.push(MissionDef { portals: 2, rivers: vec![river(false, true, 0.6, 4, 2)], islands: vec![island((0.45, 0.5), false, true, 2000)], rivals: vec![(0.84, 0.25, 1, 2), (0.84, 0.75, 1, 3)], ..base("m28", (144, 136), 2828, vec![Goal::Rivals]) }); v.push(MissionDef { portals: 2, rivers: vec![river(true, true, 0.5, 4, 1)], forts: vec![fort((0.84, 0.5), (25, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 3, 14, 3, 4, 3, 2500).heart().boss(Paladin, 5)], ..base("m18", (144, 144), 1818, vec![Goal::Heart]) }); v.push(MissionDef { portals: 2, rivers: vec![river(false, true, 0.42, 4, 2), river(true, false, 0.5, 4, 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", (152, 144), 1919, vec![Goal::Shards]) }); v.push(MissionDef { portals: 2, rivers: vec![river(true, true, 0.62, 4, 1)], islands: vec![island((0.42, 0.22), true, true, 2500), island((0.45, 0.78), true, true, 2500)], caves: vec![cave((0.5, 0.5), Some((Wyrm, 5)), 2000)], forts: vec![fort((0.85, 0.5), (21, 21), &[3], &[Arrow, Cannon, Magic, Magic], 3, 12, 2, 4, 2, 2500).shard()], ..base("m29", (152, 152), 2929, vec![Goal::Shards, Goal::Springs(2)]) }); 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", (160, 160), 2020, vec![Goal::Heart, Goal::Shards]) }); v.push(MissionDef { portals: 3, gold: 6000.0, gate: true, waves: true, first_wave: 420.0, gate_defenders: true, rivers: vec![river(false, true, 0.45, 4, 2), river(true, false, 0.5, 4, 2)], islands: vec![island((0.3, 0.2), false, true, 3000)], rivals: vec![(0.85, 0.22, 2, 2), (0.85, 0.78, 2, 3)], ..base("m30", (176, 176), 3030, vec![Goal::Rivals, Goal::Heart]) }); 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, } } } // keeps the core area, portals, gates and fortresses (rock is carved away) fn free_for_terrain(t: &Tile) -> bool { t.owner == Owner::None && !matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard) } // an island ringed by four tiles of water or lava fn place_island(g: &mut Game, d: &IslandDef, w: usize, h: usize, seed: u32) { let (cx, cy) = ((d.at.0 * w as f32) as i32, (d.at.1 * h as f32) as i32); let outer = d.r + 4; let mut rng = Rng(seed | 1); for y in cy - outer..=cy + outer { for x in cx - outer..=cx + outer { if x < 2 || y < 2 || x >= w as i32 - 2 || y >= h as i32 - 2 { continue; } let dist = (((x - cx).pow(2) + (y - cy).pow(2)) as f32).sqrt(); let t = &mut g.map.t[idx(x, y)]; if !free_for_terrain(t) || dist > outer as f32 + 0.4 { continue; } t.gold = 0; t.marked = false; t.reinforced = false; t.kind = if dist <= d.r as f32 + 0.4 { Kind::Floor } else if d.lava { Kind::Lava } else { Kind::Water }; } } if d.spring { g.map.t[idx(cx, cy)].kind = Kind::ManaSpring; } if d.treasure > 0 { let spots = [(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)]; let per = d.treasure / spots.len() as i32; for &(x, y) in &spots { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Treasury; t.gold = per; let r = rng.f(); t.res = if r < 0.15 { 3 } else if r < 0.4 { 2 } else if r < 0.6 { 1 } else { 0 }; } } } // a small cave sealed by earth all around, with treasure and/or a creature in chains fn place_cave(g: &mut Game, d: &CaveDef, w: usize, h: usize) { let (cx, cy) = ((d.at.0 * w as f32) as i32, (d.at.1 * h as f32) as i32); for y in cy - 3..=cy + 3 { for x in cx - 3..=cx + 3 { if x < 2 || y < 2 || x >= w as i32 - 2 || y >= h as i32 - 2 { continue; } let t = &mut g.map.t[idx(x, y)]; if !free_for_terrain(t) { continue; } let inside = (x - cx).abs() <= 2 && (y - cy).abs() <= 2; t.kind = if inside { Kind::Floor } else { Kind::Earth }; t.gold = 0; t.marked = false; t.reinforced = false; t.hp = 6.0; } } if d.treasure > 0 { let spots = [(cx - 2, cy - 2), (cx + 2, cy - 2), (cx - 2, cy + 2), (cx + 2, cy + 2)]; for &(x, y) in &spots { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Treasury; t.gold = d.treasure / 4; t.res = 2; } } if let Some((kind, level)) = d.chained { let id = g.spawn(kind, false, cx as f32 + 0.5, cy as f32 + 0.5, level); if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) { e.chained = true; } } } // Builds the game of mission i (carry: what the last mission handed over) pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game { start_mission_mode(i, carry, false) } // A co-op campaign mission: the same mission, but without rival masters on playable sides. pub fn start_coop_mission(i: usize, carry: Option<&Carry>) -> Game { start_mission_mode(i, carry, true) } fn start_mission_mode(i: usize, carry: Option<&Carry>, coop: bool) -> Game { let all = missions(); let d = &all[i.min(all.len() - 1)]; start_mission_def(d, i, carry, coop) } // A mission generated from the campaign editor file: same start pipeline, but without the // built-in campaign table and without language-file story keys for the title. fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool) -> Game { 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; } for (k, isl) in d.islands.iter().enumerate() { place_island(&mut g, isl, w, h, d.seed.wrapping_add(k as u32 * 131)); } for c in &d.caves { place_cave(&mut g, c, w, h); } if !coop { for &(rx, ry, diff, side) in &d.rivals { let x = ((rx * w as f32) as i32).clamp(10, w as i32 - 10); let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10); g.add_rival(side, x, y, diff); } } g.place_dwarf_caves(d.seed); g.add_wilds(d.seed, false); g.init_research(Some(i)); g.init_fog(); let heart = d.gate || d.forts.iter().any(|f| f.heart); if !heart { g.gate_hp = 0.0; } g.gate_on = heart; g.guard_treasures(); g.place_all_torches(); let goals: Vec = if coop { d.goals.iter().filter(|&g| *g != Goal::Rivals).cloned().collect() } else { d.goals.clone() }; g.mission = Some(MissionState { index: i, goals, 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; g.rune_free += c.rune_free; g.rune_final += c.rune_final; } 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 } // ------------------------------------------------------------------ editor missions // a mission definition from editor generator settings (random maps and new painted maps) pub fn generated_def(d: &crate::campaign_doc::MissionDoc, seed: u32) -> MissionDef { let mut out = mission_def_from_doc(d); out.seed = seed; let g = &d.gen; let mut rng = Rng(seed | 1); for k in 0..g.rivers.min(3) { out.rivers.push(river(false, k % 2 == 0, 0.3 + 0.2 * k as f32 + rng.f() * 0.08, 4, 1 + (rng.next() % 2))); } for k in 0..g.lava.min(2) { out.rivers.push(river(true, k % 2 == 1, 0.62 - 0.25 * k as f32, 4, 1)); } let lvl = 1 + g.difficulty.min(2) as u32; for k in 0..g.forts.min(3) { let at = [(0.8, 0.5), (0.75, 0.2), (0.75, 0.8)][k as usize]; out.forts.push(fort(at, (17, 15), &[3], &[TowerKind::Arrow, TowerKind::Arrow], lvl, 3 + 2 * lvl, lvl, lvl, 1, 1000 * lvl as i32)); } for k in 0..g.rivals.min(2) { let (x, y) = [(0.82, 0.22), (0.82, 0.78)][k as usize]; out.rivals.push((x, y, g.difficulty.min(2), 2 + k)); } out } // the seed of a random map in one run of a campaign pub fn run_map_seed(run_seed: u32, mission: usize) -> u32 { run_seed.wrapping_mul(2654435761).wrapping_add(mission as u32 * 40503) | 1 } // a mission of an editor campaign: its painted map or a random map (rolled from the run's // seed), the goals it can actually have, and its block scripts pub fn start_editor_mission(doc: &crate::campaign_doc::MissionDoc, i: usize, run_seed: u32) -> Game { editor_mission_mode(doc, i, run_seed, false) } // the same mission for a co-op match: the other master seats (2, 3, 4) belong to the players, so // rival masters of the map are left out (like the built-in co-op missions) and so is the Rivals goal pub fn start_editor_coop_mission(doc: &crate::campaign_doc::MissionDoc, i: usize, run_seed: u32) -> Game { editor_mission_mode(doc, i, run_seed, true) } // editor missions that can be played in co-op: the master areas need a map of at least 72x72, // and a goal other than beating rival masters pub fn is_editor_coop_mission(doc: &crate::campaign_doc::MissionDoc) -> bool { doc.size.0 >= 72 && doc.size.1 >= 72 && crate::campaign_doc::parse_goals(&doc.goals).iter().any(|g| *g != Goal::Rivals) } pub fn editor_coop_indexes(doc: &crate::campaign_doc::CampaignDoc) -> Vec { doc.missions.iter().enumerate().filter(|(_, m)| is_editor_coop_mission(m)).map(|(i, _)| i).collect() } pub fn next_editor_coop_mission(doc: &crate::campaign_doc::CampaignDoc, i: usize, dir: i32) -> usize { let list = editor_coop_indexes(doc); if list.is_empty() { return 0; } let pos = list.iter().position(|m| *m == i).unwrap_or(0); list[((pos as i32 + dir).rem_euclid(list.len() as i32)) as usize] } // a painted map without its rival masters: their creatures go, their tiles become unowned fn remove_rivals(g: &mut Game) { let sides: Vec = g.rivals.iter().map(|r| r.side).collect(); if sides.is_empty() { return; } g.rivals.clear(); g.ents.retain(|e| e.hero || !sides.contains(&e.side)); let owners: Vec = sides.iter().map(|&s| Owner::of_side(s)).collect(); for t in g.map.t.iter_mut() { if owners.contains(&t.owner) { t.owner = Owner::None; if t.kind == Kind::Core { t.kind = Kind::Floor; } } } g.recount(); } fn editor_mission_mode(doc: &crate::campaign_doc::MissionDoc, i: usize, run_seed: u32, coop: bool) -> Game { use crate::campaign_doc::MapDoc; let generated = || start_mission_def(&generated_def(doc, run_map_seed(run_seed, i)), i, None, coop); let mut g = match &doc.map { MapDoc::Fixed { game } => match load_fixed_map(game) { Some(mut g) => { if coop { remove_rivals(&mut g); } g } None => generated(), }, MapDoc::Random => generated(), }; g.msgs.clear(); g.state = 0; g.time = 0.0; // goals the map cannot have would be won at once: they are left out let shards = g.map.t.iter().filter(|t| t.kind == Kind::Shard).count() as u32; let heart = g.gate_hp > 0.0 && g.gate_on; let knight_towers = g.towers.iter().any(|t| t.hero); let heroes = g.ents.iter().any(|e| e.alive && e.active_hero()) || doc.waves; let goals: Vec = crate::campaign_doc::parse_goals(&doc.goals) .into_iter() .filter(|goal| match goal { Goal::Shards => shards > 0, Goal::Heart => heart, Goal::Towers => knight_towers, Goal::KillAll => heroes, Goal::Rivals => !g.rivals.is_empty(), Goal::Survive(_) => doc.waves, _ => true, }) .collect(); g.mission = Some(MissionState { index: i, goals, tools: vec![true; crate::ui::TOOLS.len()], waves: doc.waves, wave_limit: doc.wave_limit, gate_defenders: doc.waves, heart, time_limit: doc.time_limit, shards, }); g.wave_timer = doc.first_wave; g.gold = doc.gold; if !doc.scripts.is_empty() { g.script = Some(crate::mscript::ScriptState::new(doc.scripts.clone(), doc.zones.clone(), run_map_seed(run_seed, i))); } if !doc.title.is_empty() { g.msg("msg.mission_start_custom", vec![doc.title.clone()]); } g } // the map editor sets a knight fortress with its middle at (x, y); level 1..3, with a shard pub fn editor_fort(g: &mut Game, x: i32, y: i32, level: u32, shard: bool) { let (w, h) = (g.map.w, g.map.h); let lvl = level.clamp(1, 3); let mut f = fort((x as f32 / w as f32, y as f32 / h as f32), (15 + 2 * lvl as i32, 13 + 2 * lvl as i32), &[3], &[TowerKind::Arrow, TowerKind::Arrow], lvl, 2 + 2 * lvl, lvl, lvl, 1, 800 * lvl as i32); f.shard = shard; g.build_fortress(&f.spec(w, h)); g.recount(); } // a painted map: the game state saved by the map editor pub fn load_fixed_map(json: &str) -> Option { let mut g: Game = serde_json::from_str(json).ok()?; g.map.activate(); g.recount(); Some(g) } // missions that can be played in co-op: they fit the multiplayer master areas and do not // depend on rival masters as their only goal pub fn is_coop_mission(i: usize) -> bool { missions().get(i).map_or(false, |d| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals)) } pub fn coop_mission_indexes() -> Vec { missions() .iter() .enumerate() .filter(|(_, d)| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals)) .map(|(i, _)| i) .collect() } pub fn next_coop_mission(i: usize, dir: i32) -> usize { let list = coop_mission_indexes(); if list.is_empty() { return 0; } let pos = list.iter().position(|m| *m == i).unwrap_or(0); list[((pos as i32 + dir).rem_euclid(list.len() as i32)) as usize] } // 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(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next } } #[cfg(test)] mod tests { use super::*; #[test] fn a_custom_editor_mission_can_be_started() { let doc = crate::campaign_doc::MissionDoc { key: "custom01".into(), title: "Custom".into(), size: (72, 72), seed: 9991, gold: 3500.0, goals: vec!["KillAll".into()], ..Default::default() }; let def = mission_def_from_doc(&doc); assert_eq!(def.key, "custom01"); assert_eq!(def.goals, vec![Goal::KillAll]); let g = start_mission_def(&def, 0, None, false); let m = g.mission.as_ref().expect("the custom mission has mission state"); assert_eq!(m.goals, vec![Goal::KillAll]); assert_eq!(g.gold, 3500.0); } #[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), Goal::Rivals => assert!(!g.rivals.is_empty(), "{}", d.key), Goal::Springs(n) => { let springs = g.map.t.iter().filter(|t| t.kind == Kind::ManaSpring).count(); assert!(springs as u32 >= *n, "{}: {} springs", d.key, springs); } Goal::Freed(n) => { let chained = g.ents.iter().filter(|e| e.alive && e.chained).count(); assert!(chained as u32 >= *n, "{}: {} chained", d.key, chained); } } } // rival masters stand with an intact core for r in &g.rivals { let t = &g.map.t[idx(r.core.0 + 1, r.core.1 + 1)]; assert!(t.kind == Kind::Core && t.owner == Owner::of_side(r.side), "{}: rival core broken", d.key); println!(" rival {} at {:?}", r.side, r.core); } // 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); } } #[test] fn coop_missions_drop_rivals_and_are_not_decided_at_once() { let list = coop_mission_indexes(); assert!(list.len() > 3, "co-op campaign needs selectable missions"); for &i in &list { assert!(is_coop_mission(i), "mission {} selectable in co-op", i); let g = start_coop_mission(i, None); assert!(g.rivals.is_empty(), "mission {} must not spawn rival masters", i); let m = g.mission.as_ref().unwrap(); assert!(!m.goals.iter().any(|g| *g == Goal::Rivals), "mission {} keeps the rival goal", i); assert!(!m.goals.is_empty(), "mission {} has a playable goal after dropping rivals", i); assert!(!m.goals.iter().all(|&goal| g.goal_done(goal)), "mission {} is decided at the start", i); } } // 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), _ 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)); } }