221 lines
7.5 KiB
Rust
221 lines
7.5 KiB
Rust
// 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);
|
|
}
|
|
}
|