// Research: creatures studying in the library collect research points, and rooms, spells, // towers, traps and doors are discovered one after another in a fixed order. Free play starts // with the basics; a campaign mission knows what its story allows and may research what the // next few missions would bring. use crate::game::*; // what is researched, in this order: (index in ui::TOOLS, research points) pub const RESEARCH: &[(usize, f32)] = &[ (5, 200.0), // training hall (10, 250.0), // heal (34, 250.0), // haste (21, 300.0), // forge (22, 250.0), // spike pit (29, 250.0), // wooden door (32, 300.0), // seer's eye (15, 400.0), // cannon tower (42, 300.0), // guard post (12, 450.0), // torture chamber (40, 450.0), // prison (26, 300.0), // alarm bell (35, 400.0), // stoneskin (11, 500.0), // summon (13, 550.0), // temple (23, 500.0), // poison vent (30, 500.0), // iron door (33, 550.0), // cave-in (18, 550.0), // aerie (43, 550.0), // arena (41, 600.0), // graveyard (16, 650.0), // magic tower (36, 600.0), // gold rain (24, 650.0), // fire jet (25, 650.0), // stone roller (37, 700.0), // invisibility (19, 700.0), // steel walls (47, 700.0), // drawbridge (28, 800.0), // frost glow (48, 800.0), // frost spell (38, 850.0), // plague (27, 900.0), // rune mine (39, 950.0), // quake (44, 900.0), // sacrifice altar (31, 1100.0), // magic door (50, 900.0), // battering ram (51, 1000.0), // catapult ]; // room levels, researched once everything else is known: (index in ROOM_KINDS, level, points) pub const UPGRADES: &[(usize, u8, f32)] = &[ (1, 2, 500.0), // lair (2, 2, 500.0), // mushroom garden (0, 2, 550.0), // treasury (4, 2, 600.0), // library (3, 2, 600.0), // training hall (8, 2, 650.0), // forge (6, 2, 650.0), // temple (7, 2, 650.0), // aerie (1, 3, 1000.0), // lair (2, 3, 1000.0), // mushroom garden (0, 3, 1100.0), // treasury (4, 3, 1200.0), // library (3, 3, 1200.0), // training hall (8, 3, 1300.0), // forge (6, 3, 1300.0), // temple (7, 3, 1300.0), // aerie ]; // known from the start in free play pub const BASE_TOOLS: &[usize] = &[0, 1, 2, 3, 4, 6, 7, 8, 9, 14, 17, 20, 45, 46, 49]; // campaign: what the next missions bring may be researched early const LOOKAHEAD: usize = 4; impl Game { // free play (None) or campaign mission i: what is known, what can be researched pub fn init_research(&mut self, mission: Option) { let n = crate::ui::TOOLS.len(); match mission { None => { self.known = (0..n).map(|i| BASE_TOOLS.contains(&i)).collect(); self.researchable = vec![true; n]; } Some(m) => { let unlock = |i: usize| crate::campaign::TOOL_UNLOCK.get(i).copied().unwrap_or(0); self.known = (0..n).map(|i| unlock(i) <= m).collect(); self.researchable = (0..n).map(|i| unlock(i) <= m + LOOKAHEAD && i != crate::game::RUNE_TOOL).collect(); } } self.research = 0.0; } pub fn known(&self, tool: usize) -> bool { self.known.get(tool).copied().unwrap_or(true) } // the item being researched now: (index in RESEARCH) pub fn research_target(&self) -> Option { if self.known.is_empty() { return None; } RESEARCH.iter().position(|&(t, _)| !self.known(t) && self.researchable.get(t).copied().unwrap_or(false)) } // the next items after the current one (for the library's info) pub fn research_queue(&self, n: usize) -> Vec { RESEARCH.iter().enumerate().filter(|(_, &(t, _))| !self.known(t) && self.researchable.get(t).copied().unwrap_or(false)).map(|(k, _)| k).take(n).collect() } // could this tool still be researched (for the locked-tool message)? pub fn can_research(&self, tool: usize) -> bool { !self.known(tool) && self.researchable.get(tool).copied().unwrap_or(false) && RESEARCH.iter().any(|&(t, _)| t == tool) } // the room level being researched once no tool is left: index in UPGRADES pub fn upgrade_target(&self) -> Option { if self.research_target().is_some() || self.known.is_empty() { return None; } UPGRADES.iter().position(|&(r, lv, _)| self.room_level.get(r).copied().unwrap_or(1).max(1) < lv) } pub(crate) fn add_research(&mut self, points: f32) { if self.research_target().is_some() || self.upgrade_target().is_some() { self.research += points; } } // a room kind reached level `lv` (the map keeps it for the look of the rooms) pub fn set_room_level(&mut self, r: usize, lv: u8) { if self.room_level.len() < crate::game::N_ROOMS { self.room_level.resize(crate::game::N_ROOMS, 1); } self.room_level[r] = lv; let side = self.me_side; self.map.upgrades.retain(|u| !(u.0 == side && u.1 as usize == r)); if lv > 1 { self.map.upgrades.push((side, r as u8, lv)); } } pub(crate) fn update_research(&mut self) { if let Some(u) = self.upgrade_target() { let (r, lv, cost) = UPGRADES[u]; if self.research >= cost { self.research -= cost; self.set_room_level(r, lv); let roman = if lv == 2 { "II" } else { "III" }; self.msg("msg.room_upgraded", vec![format!("@{}", crate::game::ROOM_NAMES[r]), roman.to_string()]); self.sfx_ui(Sfx::LevelUp); } return; } let Some(k) = self.research_target() else { return }; let (tool, cost) = RESEARCH[k]; if self.research >= cost { self.research -= cost; if let Some(v) = self.known.get_mut(tool) { *v = true; } let name = crate::ui::tool_info(crate::ui::TOOLS[tool]).name; self.msg("msg.researched", vec![name]); self.sfx_ui(Sfx::LevelUp); } } } #[cfg(test)] mod tests { use super::*; use crate::map::*; #[test] fn studying_discovers_the_next_item() { let mut g = Game::new(9, 64, 64); assert!(!g.tool_allowed(5), "training hall must be researched"); assert!(g.tool_allowed(2) && g.tool_allowed(6)); let (cx, cy) = g.map.core; for y in cy - 8..=cy - 5 { for x in cx..=cx + 4 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Library; t.owner = Owner::Player; } } g.recount(); for k in 0..4 { g.spawn(EK::Mage, false, cx as f32 + 0.5 + k as f32, cy as f32 - 6.5, 3); } let mut found = false; for _ in 0..(20 * 400) { g.update(0.05); if g.tool_allowed(5) { found = true; break; } } assert!(found, "research points {}", g.research); } #[test] fn missions_know_their_tools_and_research_ahead() { let g = crate::campaign::start_mission(2, None); assert!(g.tool_allowed(5), "training hall known from mission 3"); assert!(!g.tool_allowed(12)); assert!(g.can_research(10), "heal comes soon and can be researched"); assert!(!g.can_research(31), "magic doors are far away"); } #[test] fn siege_engines_are_not_known_in_the_first_missions() { let g = crate::campaign::start_mission(0, None); for t in [19, 47, 48, 50, 51] { assert!(!g.tool_allowed(t), "tool {} known in mission 1", t); } } #[test] fn once_everything_is_known_the_library_raises_room_levels() { let mut g = Game::new(9, 64, 64); assert!(g.upgrade_target().is_none(), "tools first"); for v in g.known.iter_mut() { *v = true; } let u = g.upgrade_target().expect("a room level to research"); let (r, lv, cost) = UPGRADES[u]; assert_eq!(lv, 2); let before = g.room_bonus(ROOM_KINDS[r]); g.research = cost + 1.0; g.update_research(); assert_eq!(g.room_level_of(ROOM_KINDS[r]), 2); assert!(g.room_bonus(ROOM_KINDS[r]) > before); assert!(g.map.upgrades.iter().any(|x| x.1 as usize == r && x.2 == 2), "the map shows it"); assert_ne!(g.upgrade_target(), Some(u), "the next one"); } }