237 lines
8.5 KiB
Rust
237 lines
8.5 KiB
Rust
// 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<usize>) {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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");
|
|
}
|
|
}
|