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:
1 parent
b93bcd89c4
commit
bd956a4f57
28 files changed
+720
-22
No files matched your search
@@ -54,6 +54,7 @@ impl Game {
|
||||
self.ai_rooms(cx, cy);
|
||||
self.ai_towers(d, cx, cy);
|
||||
self.ai_fight(k, d, side, cx, cy);
|
||||
self.ai_talents();
|
||||
}
|
||||
|
||||
// the tiles an AI master may mark: within MARK_REACH tiles of its own ground
|
||||
|
||||
+6
-2
@@ -84,6 +84,8 @@ pub struct Carry {
|
||||
pub rune_free: u32, // unbuilt rune forge towers from chests
|
||||
#[serde(default)]
|
||||
pub rune_final: u32, // unbuilt or sold rune forge towers that were already given back once
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents, // the master's talents go along from mission to mission
|
||||
}
|
||||
|
||||
pub const CARRY_CREATURES: usize = 5;
|
||||
@@ -580,6 +582,7 @@ fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool
|
||||
g.gold += c.gold;
|
||||
g.rune_free += c.rune_free;
|
||||
g.rune_final += c.rune_final;
|
||||
g.talents = c.talents.clone();
|
||||
}
|
||||
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
|
||||
let a = k as f32 * 0.9;
|
||||
@@ -796,7 +799,7 @@ pub fn carry_from(g: &Game) -> Carry {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval;
|
||||
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next }
|
||||
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next, talents: g.talents.clone() }
|
||||
}
|
||||
|
||||
// a co-op match of an editor campaign: what every master takes into the next mission - his
|
||||
@@ -810,7 +813,7 @@ pub fn coop_carry_from(g: &Game) -> Vec<(u8, Carry)> {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && !e.hero && e.side == side && e.kind != EK::Worker && !e.chained && e.captive == 0).collect();
|
||||
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal).then(a.id.cmp(&b.id)));
|
||||
let creatures = c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect();
|
||||
(side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0 })
|
||||
(side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0, talents: g.talents_of(side) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -824,6 +827,7 @@ pub fn apply_coop_carry(g: &mut Game, carry: &[(u8, Carry)]) {
|
||||
}
|
||||
g.switch_to(*side);
|
||||
g.gold += c.gold.max(0.0);
|
||||
g.talents = c.talents.clone();
|
||||
let (cx, cy) = g.map.core;
|
||||
for (k, &(kind, level, xp)) in c.creatures.iter().take(CARRY_CREATURES).enumerate() {
|
||||
let a = k as f32 * 1.1;
|
||||
|
||||
+6
-1
@@ -101,6 +101,10 @@ pub enum Act {
|
||||
to: u8,
|
||||
amount: u32,
|
||||
},
|
||||
// spend a talent point on a branch (talents.rs)
|
||||
Talent {
|
||||
branch: u8,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -148,7 +152,7 @@ impl Game {
|
||||
|
||||
fn mark_command(&mut self, act: Act, side: u8) {
|
||||
let ((x, y), kind, dur) = match act {
|
||||
Act::SendGold { .. } => return, // nothing to point at on the map
|
||||
Act::SendGold { .. } | Act::Talent { .. } => return, // nothing to point at on the map
|
||||
Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0),
|
||||
Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0),
|
||||
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
|
||||
@@ -227,6 +231,7 @@ impl Game {
|
||||
Act::CancelOrders => self.cancel_orders(),
|
||||
Act::Unmark { x, y } => self.unmark(x, y),
|
||||
Act::SendGold { to, amount } => self.send_gold(to, amount),
|
||||
Act::Talent { branch } => self.learn_talent(branch as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-14
@@ -523,6 +523,8 @@ pub struct MasterState {
|
||||
pub rune_next: u32,
|
||||
pub portal_timer: f32,
|
||||
pub punch_ready: f32,
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -640,6 +642,8 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub snares: Vec<usize>, // tiles with a ranger's snare (rogues.rs)
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents, // of the master being processed (talents.rs)
|
||||
#[serde(default)]
|
||||
pub guard_marks: Vec<usize>, // guard markers (shields) of the master being processed (orders.rs)
|
||||
#[serde(default)]
|
||||
pub attack_marks: Vec<usize>, // attack markers (swords)
|
||||
@@ -990,6 +994,7 @@ impl Game {
|
||||
trading: Default::default(),
|
||||
happenings: None,
|
||||
snares: Vec::new(),
|
||||
talents: Default::default(),
|
||||
guard_marks: Vec::new(),
|
||||
attack_marks: Vec::new(),
|
||||
marks_t: 0.0,
|
||||
@@ -1218,7 +1223,7 @@ impl Game {
|
||||
|
||||
// what the next payday costs: wages of all creatures plus the training bill
|
||||
pub fn next_payday_cost(&self) -> f32 {
|
||||
let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum::<f32>() * self.talents.wages();
|
||||
wages + self.training_bill
|
||||
}
|
||||
|
||||
@@ -1514,6 +1519,7 @@ impl Game {
|
||||
rune_next: self.rune_next,
|
||||
portal_timer: self.portal_timer,
|
||||
punch_ready: self.punch_ready,
|
||||
talents: self.talents.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1549,6 +1555,7 @@ impl Game {
|
||||
self.rune_next = m.rune_next;
|
||||
self.portal_timer = m.portal_timer;
|
||||
self.punch_ready = m.punch_ready;
|
||||
self.talents = m.talents;
|
||||
}
|
||||
|
||||
// make `side` the master being processed (no-op for sides that are no masters)
|
||||
@@ -1734,7 +1741,7 @@ impl Game {
|
||||
// a master's household: mana, core, portals, payday
|
||||
fn economy(&mut self, dt: f32) {
|
||||
self.recount();
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max());
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * self.talents.mana_rate() * dt).min(self.mana_max());
|
||||
self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP);
|
||||
|
||||
let owned = self.portals_owned();
|
||||
@@ -2081,13 +2088,14 @@ impl Game {
|
||||
self.training_bill = 0.0;
|
||||
let mut n = 0;
|
||||
let mut total = bill;
|
||||
let merchant = self.talents.wages();
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || !self.ents[i].creature() {
|
||||
continue;
|
||||
}
|
||||
let r = self.rng.f();
|
||||
let e = &mut self.ents[i];
|
||||
let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32);
|
||||
let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32) * merchant;
|
||||
e.pay_due += w;
|
||||
e.pay_t = r * 55.0;
|
||||
n += 1;
|
||||
@@ -2761,7 +2769,9 @@ impl Game {
|
||||
}
|
||||
|
||||
fn hit(&mut self, e: &mut Ent, j: usize) {
|
||||
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
|
||||
// the Warlord talent of the attacker's master
|
||||
let boost = if !e.hero && Owner::MASTER_SIDES.contains(&e.side) { self.talents_of(e.side).damage() } else { 1.0 };
|
||||
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3) * boost;
|
||||
let (tx, ty) = (self.ents[j].x, self.ents[j].y);
|
||||
let s = st(e.kind);
|
||||
// the higher the level, the bigger the attack looks (up to almost twice at Lv 10)
|
||||
@@ -2870,8 +2880,12 @@ impl Game {
|
||||
}
|
||||
let my_id = e.id;
|
||||
e.invis = 0.0; // attacking breaks invisibility
|
||||
let armour = {
|
||||
let t = &self.ents[j];
|
||||
if !t.hero && Owner::MASTER_SIDES.contains(&t.side) { self.talents_of(t.side).armour() } else { 1.0 }
|
||||
};
|
||||
let t = &mut self.ents[j];
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg };
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour;
|
||||
t.flash = 0.15;
|
||||
if t.creature() && !matches!(t.job, Job::Attack(_)) {
|
||||
t.job = Job::Attack(my_id);
|
||||
@@ -2946,6 +2960,7 @@ impl Game {
|
||||
}
|
||||
if hero {
|
||||
let b = s.bounty as f32 + if kind == EK::Thief { self.thief_loot(id) } else { 0.0 };
|
||||
self.add_master_xp(s.bounty as f32 * crate::talents::HERO_XP);
|
||||
let got = self.add_gold(b);
|
||||
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
|
||||
self.sfx(Sfx::HeroDeath, x, y);
|
||||
@@ -3495,7 +3510,7 @@ impl Game {
|
||||
e.face = sgn(tx as f32 + 0.5 - e.x);
|
||||
e.working = true;
|
||||
let skilled = if crate::personality::knows(e, "dig") { 1.25 } else { 1.0 };
|
||||
e.timer += dt * self.help_speed(t) * skilled;
|
||||
e.timer += dt * self.help_speed(t) * skilled * self.talents.dig_speed();
|
||||
if e.timer >= 0.45 {
|
||||
e.timer = 0.0;
|
||||
let fxx = (e.x + tx as f32 + 0.5) * 0.5;
|
||||
@@ -3655,7 +3670,7 @@ impl Game {
|
||||
}
|
||||
Job::Deliver => {
|
||||
if self.is_deposit(cur) {
|
||||
let amount = e.carry as f32;
|
||||
let amount = e.carry as f32 * self.talents.treasure();
|
||||
let got = self.add_res_at(amount, e.carry_res, Some(cur));
|
||||
if got < amount {
|
||||
self.hint(3, "hint.treasury_full");
|
||||
@@ -4939,7 +4954,7 @@ impl Game {
|
||||
|
||||
pub fn build_rect(&mut self, room: usize, x0: i32, y0: i32, x1: i32, y1: i32) {
|
||||
let kind = ROOM_KINDS[room];
|
||||
let cost = ROOM_COST[room] as f32;
|
||||
let cost = (ROOM_COST[room] as f32 * self.talents.room_cost()).round();
|
||||
let mut built = 0;
|
||||
let mut broke = false;
|
||||
for y in y0.min(y1)..=y0.max(y1) {
|
||||
@@ -5693,13 +5708,13 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn lightning(&mut self, wx: f32, wy: f32) {
|
||||
const COST: f32 = 50.0;
|
||||
if self.mana < COST {
|
||||
let cost = 50.0 * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_bolt", vec![]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.mana -= COST;
|
||||
self.mana -= cost;
|
||||
self.count(crate::stats::SPELLS, 1.0);
|
||||
self.sfx(Sfx::Lightning, wx, wy);
|
||||
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||
@@ -5720,13 +5735,13 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn heal(&mut self, wx: f32, wy: f32) {
|
||||
const COST: f32 = 60.0;
|
||||
if self.mana < COST {
|
||||
let cost = 60.0 * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_heal", vec![]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.mana -= COST;
|
||||
self.mana -= cost;
|
||||
self.count(crate::stats::SPELLS, 1.0);
|
||||
self.sfx(Sfx::Heal, wx, wy);
|
||||
let mut fxs = Vec::new();
|
||||
|
||||
@@ -30,6 +30,7 @@ mod props;
|
||||
mod research;
|
||||
mod rival;
|
||||
mod rogues;
|
||||
mod talents;
|
||||
mod render3d;
|
||||
mod rooms;
|
||||
mod save;
|
||||
@@ -1821,6 +1822,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
}
|
||||
UiAction::OpenMenu => menu.page = Some(Page::Pause),
|
||||
UiAction::SendGold(to, amount) => app.game.queue(Act::SendGold { to, amount }),
|
||||
UiAction::LearnTalent(branch) => app.game.queue(Act::Talent { branch }),
|
||||
UiAction::Locked => {
|
||||
let locked = ui.last_locked.unwrap_or(usize::MAX);
|
||||
app.game.msg(if app.game.can_research(locked) { "msg.tool_research" } else { "msg.tool_locked" }, vec![]);
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn cast(&mut self, spell: Spell, wx: f32, wy: f32) {
|
||||
let cost = spell.cost();
|
||||
let cost = spell.cost() * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
|
||||
@@ -48,6 +48,8 @@ impl GameStats {
|
||||
impl Game {
|
||||
// counts for the master at this computer (another master's deeds count on his computer)
|
||||
pub(crate) fn count(&mut self, k: usize, n: f32) {
|
||||
// every master's deeds bring him experience for his talents
|
||||
self.add_master_xp(crate::talents::xp_for(k) * n);
|
||||
if self.me_side != self.local_side || self.demo.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
+220
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -457,6 +457,8 @@ enum TBtn {
|
||||
Tool(usize),
|
||||
Menu,
|
||||
Overview,
|
||||
Talents, // the master's talent window
|
||||
Learn(u8), // spend a point on a branch
|
||||
OvRow(u32),
|
||||
OvScroll(i32),
|
||||
GoldTo, // the next ally to receive gold
|
||||
@@ -478,6 +480,7 @@ pub enum UiAction {
|
||||
ChatSetTeam(bool),
|
||||
ChatSend,
|
||||
SendGold(u8, u32), // gold for an ally of a match (side, amount)
|
||||
LearnTalent(u8), // spend a talent point on a branch
|
||||
Consumed,
|
||||
}
|
||||
|
||||
@@ -485,6 +488,7 @@ pub struct Ui {
|
||||
pub tool: Tool,
|
||||
pub show_help: bool,
|
||||
pub show_overview: bool, // list of all creatures
|
||||
pub show_talents: bool, // the master's talents
|
||||
pub coop_end: Option<(bool, bool, bool)>, // end of an editor co-op mission: (host, a next mission, asked for it)
|
||||
gold_to: usize, // the ally chosen in the creature overview
|
||||
ov_scroll: usize,
|
||||
@@ -547,6 +551,28 @@ pub fn wrap(s: &str, cols: usize) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
// what a talent branch gives at its level now, and at the next level
|
||||
fn branch_text(t: &crate::talents::Talents, b: usize) -> String {
|
||||
use crate::talents::*;
|
||||
let desc = |lv: u8| {
|
||||
let l = lv as i32;
|
||||
let (a, c) = match b {
|
||||
BUILDER => (10 * l, 6 * l),
|
||||
WARLORD => (5 * l, 5 * l),
|
||||
WARLOCK => (15 * l, 6 * l),
|
||||
_ => (6 * l, 6 * l),
|
||||
};
|
||||
trf(&format!("{}.desc", NAMES[b]), &[a.to_string(), c.to_string()])
|
||||
};
|
||||
let lv = t.lv[b];
|
||||
let now = if lv == 0 { tr("talent.none") } else { desc(lv) };
|
||||
if lv < MAX_LEVEL {
|
||||
format!("{} {}", now, trf("talent.then", &[desc(lv + 1)]))
|
||||
} else {
|
||||
now
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod wrap_tests {
|
||||
#[test]
|
||||
@@ -573,6 +599,7 @@ impl Ui {
|
||||
tool: Tool::Dig,
|
||||
show_help: true,
|
||||
show_overview: false,
|
||||
show_talents: false,
|
||||
coop_end: None,
|
||||
gold_to: 0,
|
||||
ov_scroll: 0,
|
||||
@@ -664,9 +691,17 @@ impl Ui {
|
||||
TBtn::Overview => {
|
||||
self.open_cat = None;
|
||||
self.show_overview = !self.show_overview;
|
||||
self.show_talents = false;
|
||||
self.ov_scroll = 0;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Talents => {
|
||||
self.open_cat = None;
|
||||
self.show_talents = !self.show_talents;
|
||||
self.show_overview = false;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Learn(b) => UiAction::LearnTalent(b),
|
||||
TBtn::OvScroll(d) => {
|
||||
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
||||
UiAction::Consumed
|
||||
@@ -891,7 +926,7 @@ impl Ui {
|
||||
let mut popup_top = tb_y;
|
||||
// categories + menu button; a category with several tools opens a row above
|
||||
// (the same layout for mouse and touch; on the desktop the keys still select tools directly)
|
||||
let n = CATS.len() + 2;
|
||||
let n = CATS.len() + 3;
|
||||
let bcols = (((cols - 2) as usize / n) as f32).clamp(4.0, 8.0);
|
||||
let bw = bcols * CW();
|
||||
let x0 = grid_center(sw, bw * n as f32);
|
||||
@@ -932,10 +967,27 @@ impl Ui {
|
||||
let ol: String = ol.chars().take(maxc).collect();
|
||||
gfx.text(&ol, bx + (bw - gfx.measure(&ol, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
||||
self.touch_btns.push((r, TBtn::Overview));
|
||||
// menu button
|
||||
// the master's talents: the frame pulses while points are free
|
||||
let bx = x0 + (CATS.len() + 1) as f32 * bw;
|
||||
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||
let free = g.talents.free();
|
||||
let glow = free > 0 && ((now * 3.0) as i64) % 2 == 0;
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_talents { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if glow { GOLD } else if self.show_talents { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
||||
let isz = 2.0 * CH() - 2.0 * px;
|
||||
draw_texture_ex(&gfx.atlas, bx + (bw - isz) * 0.5, by + 2.0 * px, WHITE, DrawTextureParams { source: Some(Gfx::cell(I_RUNE)), dest_size: Some(vec2(isz, isz)), ..Default::default() });
|
||||
if free > 0 {
|
||||
gfx.text(&free.to_string(), r.x + 3.0 * px, r.y + 3.0 * px, SMALL(), if glow { WHITE } else { GOLD });
|
||||
}
|
||||
let tl = tr("cat.master");
|
||||
let tl: String = tl.chars().take(maxc).collect();
|
||||
gfx.text(&tl, bx + (bw - gfx.measure(&tl, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
||||
self.touch_btns.push((r, TBtn::Talents));
|
||||
// menu button
|
||||
let bx = x0 + (CATS.len() + 2) as f32 * bw;
|
||||
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
|
||||
gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
||||
@@ -1204,6 +1256,9 @@ impl Ui {
|
||||
if self.show_overview {
|
||||
self.draw_overview(g, gfx, tb_y);
|
||||
}
|
||||
if self.show_talents {
|
||||
self.draw_talents(g, gfx, tb_y);
|
||||
}
|
||||
|
||||
// ---------------- Tooltip
|
||||
if let Some(i) = hover_tool {
|
||||
@@ -1232,6 +1287,55 @@ impl Ui {
|
||||
}
|
||||
|
||||
// all creatures, grouped by kind: level, mood, wage, job, health; a click jumps there
|
||||
// the master's talents: four branches of five levels, points from experience
|
||||
fn draw_talents(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
||||
use crate::talents::*;
|
||||
let (mx, my) = mouse();
|
||||
let t = &g.talents;
|
||||
let cols = ((sw() / CW()) as i32 - 2).min(48);
|
||||
let inner = (cols - 4) as usize;
|
||||
// the lines of each branch: name and level, what it gives now (wrapped)
|
||||
let rows_of = |b: usize| wrap(&branch_text(t, b), inner.saturating_sub(6)).len() as i32 + 2;
|
||||
let rows = 5 + (0..BRANCHES).map(rows_of).sum::<i32>();
|
||||
let x = grid_center(sw(), cols as f32 * CW()).max(0.0);
|
||||
let y = (bottom - rows as f32 * CH() - CH()).max(CH() * 4.0);
|
||||
gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("talent.title"), FRAME_HI);
|
||||
self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
||||
let ix = x + CW() * 2.0;
|
||||
let mut yy = y + CH();
|
||||
// points and the way to the next one
|
||||
gfx.text(&trf("talent.points", &[t.free().to_string()]), ix, yy, S(), if t.free() > 0 { GOLD } else { TXT });
|
||||
yy += CH() * 1.2;
|
||||
match t.progress() {
|
||||
Some((have, need)) => {
|
||||
ascii_bar(gfx, ix, yy, 16, (have / need).clamp(0.0, 1.0), MANA, SMALL());
|
||||
gfx.text(&trf("talent.next", &[(have as i32).to_string(), (need as i32).to_string()]), ix + CW() * 17.0, yy, SMALL(), DIM);
|
||||
}
|
||||
None => gfx.text(&tr("talent.all"), ix, yy, SMALL(), DIM),
|
||||
}
|
||||
yy += CH() * 1.8;
|
||||
for b in 0..BRANCHES {
|
||||
let lv = t.lv[b];
|
||||
let pips: String = (0..MAX_LEVEL).map(|k| if k < lv { '■' } else { '·' }).collect();
|
||||
gfx.text(&format!("{} {}", tr(NAMES[b]), pips), ix, yy, S(), FRAME_HI);
|
||||
// a button to learn the next level
|
||||
if t.can_learn(b) {
|
||||
let r = Rect::new(x + (cols - 7) as f32 * CW(), yy - CH() * 0.2, CW() * 4.0, CH() * 1.6);
|
||||
let hov = r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, GOLD);
|
||||
gfx.text("+", r.x + (r.w - gfx.measure("+", S())) * 0.5, r.y + (r.h - GH * S()) * 0.5, S(), GOLD);
|
||||
self.touch_btns.push((r, TBtn::Learn(b as u8)));
|
||||
}
|
||||
yy += CH() * 1.2;
|
||||
for l in wrap(&branch_text(t, b), inner.saturating_sub(6)) {
|
||||
gfx.text(&l, ix + CW() * 2.0, yy, SMALL(), DIM);
|
||||
yy += CH();
|
||||
}
|
||||
yy += CH() * 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
||||
let (sw, _) = (sw(), sh());
|
||||
let (mx, my) = mouse();
|
||||
|
||||
Reference in new issue
Block a user