Master talents: experience, points and four branches (Builder, Warlord, Warlock, Merchant) with a toolbar window

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 11:35:11 +01:00
1 parent b93bcd89c4
commit bd956a4f57
28 files changed
+720 -22

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
// 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);
}
}