// Master talents: a master earns experience (slain heroes, dug tiles, rooms, walls, traps, // towers, spells, gold) and with it talent points, spent on four branches of five levels each: // Builder (imps dig faster, rooms cost less), Warlord (creatures hit harder and take less // damage), Warlock (more mana, cheaper spells), Merchant (lower wages, more gold from treasure). // Per game in free play and matches; in the campaign they go along from mission to mission. use crate::game::*; use serde::{Deserialize, Serialize}; pub const BRANCHES: usize = 4; pub const BUILDER: usize = 0; pub const WARLORD: usize = 1; pub const WARLOCK: usize = 2; pub const MERCHANT: usize = 3; pub const MAX_LEVEL: u8 = 5; pub const NAMES: [&str; BRANCHES] = ["talent.builder", "talent.warlord", "talent.warlock", "talent.merchant"]; // experience for the deeds of a master (stats counter -> experience per unit) pub fn xp_for(counter: usize) -> f32 { use crate::stats as k; match counter { k::DUG => 2.0, k::ROOMS => 3.0, k::WALLS => 2.0, k::TRAPS => 8.0, k::TOWERS => 25.0, k::SPELLS => 5.0, k::SIEGE => 20.0, k::GOLD => 0.02, _ => 0.0, } } // experience for a slain hero: a share of his bounty pub const HERO_XP: f32 = 0.5; #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Talents { pub xp: f32, pub lv: [u8; BRANCHES], } // experience the k-th talent point needs in all (the points get dearer) pub fn point_xp(k: u32) -> f32 { (0..k).map(|i| 300.0 + 150.0 * i as f32).sum() } impl Talents { pub fn spent(&self) -> u32 { self.lv.iter().map(|&l| l as u32).sum() } // points earned so far (at most enough for every level) pub fn earned(&self) -> u32 { let max = BRANCHES as u32 * MAX_LEVEL as u32; (1..=max).take_while(|&k| self.xp >= point_xp(k)).count() as u32 } pub fn free(&self) -> u32 { self.earned().saturating_sub(self.spent()) } // progress towards the next point: (experience into it, experience it needs) pub fn progress(&self) -> Option<(f32, f32)> { let k = self.earned(); if k >= BRANCHES as u32 * MAX_LEVEL as u32 { return None; } let (a, b) = (point_xp(k), point_xp(k + 1)); Some((self.xp - a, b - a)) } pub fn can_learn(&self, branch: usize) -> bool { branch < BRANCHES && self.lv[branch] < MAX_LEVEL && self.free() > 0 } fn level(&self, branch: usize) -> f32 { self.lv.get(branch).copied().unwrap_or(0) as f32 } // Builder: imps dig and claim faster, rooms cost less pub fn dig_speed(&self) -> f32 { 1.0 + 0.1 * self.level(BUILDER) } pub fn room_cost(&self) -> f32 { 1.0 - 0.06 * self.level(BUILDER) } // Warlord: creatures hit harder and take less damage pub fn damage(&self) -> f32 { 1.0 + 0.05 * self.level(WARLORD) } pub fn armour(&self) -> f32 { 1.0 / (1.0 + 0.05 * self.level(WARLORD)) } // Warlock: mana flows faster, spells cost less pub fn mana_rate(&self) -> f32 { 1.0 + 0.15 * self.level(WARLOCK) } pub fn spell_cost(&self) -> f32 { 1.0 - 0.06 * self.level(WARLOCK) } // Merchant: lower wages, more gold from delivered treasure pub fn wages(&self) -> f32 { 1.0 - 0.06 * self.level(MERCHANT) } pub fn treasure(&self) -> f32 { 1.0 + 0.06 * self.level(MERCHANT) } } impl Game { // the talents of the master on `side` pub fn talents_of(&self, side: u8) -> Talents { if side == self.me_side { self.talents.clone() } else { self.masters.iter().find(|m| m.side == side).map(|m| m.talents.clone()).unwrap_or_default() } } // experience for the master being processed; a new talent point is announced pub(crate) fn add_master_xp(&mut self, xp: f32) { if xp <= 0.0 || self.demo.is_some() { return; } let before = self.talents.earned(); self.talents.xp += xp; if self.talents.earned() > before && self.me_side == self.local_side { self.msg("msg.talent_point", vec![]); self.sfx_ui(Sfx::LevelUp); } } // the master spends a point on a branch (an order: the same on every computer of a match) pub fn learn_talent(&mut self, branch: usize) { if !self.talents.can_learn(branch) { self.sfx_ui(Sfx::Error); return; } self.talents.lv[branch] += 1; let lv = self.talents.lv[branch]; self.msg("msg.talent_learned", vec![format!("@{}", NAMES[branch]), lv.to_string()]); self.sfx_ui(Sfx::LevelUp); } // an AI master spends his points by himself: the branches in turn, the lowest first pub(crate) fn ai_talents(&mut self) { while self.talents.free() > 0 { let order = [WARLORD, BUILDER, MERCHANT, WARLOCK]; let Some(&b) = order.iter().filter(|&&b| self.talents.lv[b] < MAX_LEVEL).min_by_key(|&&b| self.talents.lv[b]) else { break }; self.talents.lv[b] += 1; } } } #[cfg(test)] mod tests { use super::*; #[test] fn points_get_dearer_and_levels_are_capped() { let mut t = Talents::default(); assert_eq!(t.earned(), 0); t.xp = point_xp(1); assert_eq!((t.earned(), t.free()), (1, 1)); assert!(point_xp(3) - point_xp(2) > point_xp(2) - point_xp(1)); t.xp = 1e9; assert_eq!(t.earned(), 20); t.lv = [5, 5, 5, 5]; assert_eq!(t.free(), 0); assert!(!t.can_learn(0) && t.progress().is_none()); } #[test] fn deeds_bring_points_and_points_bring_talents() { let mut g = Game::new(9, 64, 64); let (cx, cy) = g.map.core; g.mark_rect(cx - 6, cy - 6, cx + 8, cy + 8, true); for _ in 0..(20 * 60 * 6) { g.update(0.05); } assert!(g.talents.xp > 100.0, "digging gives experience: {}", g.talents.xp); g.talents.xp = point_xp(2); let room0 = g.talents.room_cost(); g.learn_talent(BUILDER); g.learn_talent(WARLORD); g.learn_talent(WARLORD); // no third point assert_eq!(g.talents.lv, [1, 1, 0, 0]); assert!(g.talents.room_cost() < room0 && g.talents.damage() > 1.0); } #[test] fn talents_make_rooms_cheaper_and_wages_lower() { let mut g = Game::new(9, 64, 64); let (cx, cy) = g.map.core; for y in cy + 4..cy + 7 { for x in cx..cx + 3 { let t = &mut g.map.t[crate::map::idx(x, y)]; t.kind = crate::map::Kind::Floor; t.owner = crate::map::Owner::Player; } } g.gold = 10000.0; let gold0 = g.gold; g.build_rect(1, cx, cy + 4, cx + 2, cy + 6); let plain = gold0 - g.gold; g.sell_rect(cx, cy + 4, cx + 2, cy + 6); g.talents.lv[BUILDER] = 5; let gold1 = g.gold; g.build_rect(1, cx, cy + 4, cx + 2, cy + 6); let cheap = gold1 - g.gold; assert!(cheap < plain * 0.8, "{} < {}", cheap, plain); g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 3); let wages = g.next_payday_cost(); g.talents.lv[MERCHANT] = 5; assert!(g.next_payday_cost() < wages); } #[test] fn ai_masters_spend_their_points() { let mut g = Game::new(9, 64, 64); g.talents.xp = point_xp(6); g.ai_talents(); assert_eq!(g.talents.free(), 0); assert!(g.talents.lv.iter().all(|&l| l >= 1), "{:?}", g.talents.lv); } }