313 lines
15 KiB
Rust
313 lines
15 KiB
Rust
// The bestiary: every unit with its stats at each level, the buildings of the dungeon and the
|
|
// ranks. Crookfinger writes about his creatures with love and about the humans with disgust.
|
|
|
|
use super::*;
|
|
use crate::game::{st, tower_stats, TowerKind, EK, ROOM_COST};
|
|
use crate::ui::{tool_cost_static, BLUE as BLUE_C, GREEN as GREEN_C, RED as RED_C};
|
|
|
|
pub const TABS: [&str; 4] = ["best.tab.creatures", "best.tab.humans", "best.tab.dungeon", "best.tab.ranks"];
|
|
|
|
const CREATURES: [EK; 14] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Revenant, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
|
|
const HUMANS: [EK; 10] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin, EK::Ram, EK::Catapult];
|
|
// buildings: indices into ui::TOOLS
|
|
const BUILDINGS: [usize; 32] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47, 49, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum Entry {
|
|
Unit(EK, bool), // kind, a cave dwarf (neutral) instead of the human dwarf
|
|
Building(usize),
|
|
Rank(u32),
|
|
}
|
|
|
|
fn entries(tab: usize) -> Vec<Entry> {
|
|
match tab {
|
|
0 => CREATURES.iter().map(|&k| Entry::Unit(k, false)).collect(),
|
|
1 => {
|
|
let mut v: Vec<Entry> = HUMANS.iter().map(|&k| Entry::Unit(k, false)).collect();
|
|
v.push(Entry::Unit(EK::Dwarf, true));
|
|
v
|
|
}
|
|
2 => BUILDINGS.iter().map(|&i| Entry::Building(i)).collect(),
|
|
_ => (1..=10).map(Entry::Rank).collect(),
|
|
}
|
|
}
|
|
|
|
// language key part of a unit ("unit.kobold" -> "kobold")
|
|
fn unit_id(k: EK, cave: bool) -> String {
|
|
if cave {
|
|
"cavedwarf".into()
|
|
} else {
|
|
st(k).name.trim_start_matches("unit.").to_string()
|
|
}
|
|
}
|
|
|
|
fn entry_name(e: Entry) -> String {
|
|
match e {
|
|
Entry::Unit(_, true) => tr("best.cavedwarf.name"),
|
|
Entry::Unit(k, false) => tr(st(k).name),
|
|
Entry::Building(i) => tool_info(TOOLS[i]).name,
|
|
Entry::Rank(l) => format!("{:>2} {}", l, tr(&format!("rank.{}", l))),
|
|
}
|
|
}
|
|
|
|
fn tower_kind(i: usize) -> Option<TowerKind> {
|
|
match TOOLS[i] {
|
|
crate::ui::Tool::Tower(k) => Some(k),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// the highest level an entry can be shown at
|
|
fn max_level(e: Entry) -> u32 {
|
|
match e {
|
|
Entry::Unit(..) => 10,
|
|
Entry::Building(i) if tower_kind(i).is_some() => 3,
|
|
_ => 1,
|
|
}
|
|
}
|
|
|
|
impl Menu {
|
|
pub(super) fn open_bestiary(&mut self, from_title: bool) {
|
|
self.page = Some(Page::Bestiary { tab: 0, sel: 1, level: 1, from_title });
|
|
self.scroll = 0;
|
|
}
|
|
|
|
pub(super) fn bestiary_click(&mut self, b: Btn) {
|
|
let Some(Page::Bestiary { tab, sel, level, from_title }) = self.page else { return };
|
|
let page = match b {
|
|
Btn::BestTab(t) => Page::Bestiary { tab: t, sel: 0, level: 1, from_title },
|
|
Btn::BestSel(d) => {
|
|
let n = entries(tab).len() as i32;
|
|
let s = (sel as i32 + d).rem_euclid(n) as usize;
|
|
Page::Bestiary { tab, sel: s, level: level.min(max_level(entries(tab)[s])).max(1), from_title }
|
|
}
|
|
Btn::BestPick(s) => Page::Bestiary { tab, sel: s, level: level.min(max_level(entries(tab)[s])).max(1), from_title },
|
|
Btn::BestLevel(d) => {
|
|
let max = max_level(entries(tab)[sel]) as i32;
|
|
Page::Bestiary { tab, sel, level: (level as i32 + d).clamp(1, max) as u32, from_title }
|
|
}
|
|
_ => return,
|
|
};
|
|
self.scroll = 0;
|
|
self.page = Some(page);
|
|
}
|
|
|
|
// mouse wheel / swipe: the previous or next entry (no wrap-around)
|
|
pub(super) fn bestiary_wheel(&mut self, d: i32) {
|
|
let Some(Page::Bestiary { tab, sel, .. }) = self.page else { return };
|
|
let n = entries(tab).len() as i32;
|
|
let s = sel as i32 + d;
|
|
if s >= 0 && s < n {
|
|
self.bestiary_click(Btn::BestSel(d));
|
|
}
|
|
}
|
|
|
|
pub(super) fn draw_bestiary(&mut self, gfx: &Gfx, tab: usize, sel: usize, level: u32) {
|
|
let (sw, sh) = (sw(), sh());
|
|
let (mx, my) = mouse();
|
|
let cols = ((sw / CW()) as i32 - 2).min(78);
|
|
let rows = ((sh / CH()) as i32 - 2).min(42);
|
|
let x = grid_center(sw, cols as f32 * CW()).max(0.0);
|
|
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
|
|
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("best.title"), FRAME_HI);
|
|
// tabs
|
|
let tw = ((cols - 4) / TABS.len() as i32) as f32 * CW();
|
|
for (k, key) in TABS.iter().enumerate() {
|
|
let r = Rect::new(x + CW() * 2.0 + k as f32 * tw, y + CH() * 1.5, tw - CW() * 0.5, CH() * 2.0);
|
|
let label = tr(key);
|
|
let label: String = label.chars().take(((r.w / CW()) as usize).saturating_sub(1)).collect();
|
|
button(gfx, r, &label, r.contains(vec2(mx, my)) || k == tab, if k == tab { FRAME_HI } else { FRAME });
|
|
self.buttons.push((r, Btn::BestTab(k)));
|
|
}
|
|
let list = entries(tab);
|
|
let sel = sel.min(list.len() - 1);
|
|
let entry = list[sel];
|
|
let top = y + CH() * 4.5;
|
|
let bottom = y + (rows - 4) as f32 * CH();
|
|
// the list on the left (wide screens) or a selector row (narrow screens)
|
|
let wide = cols >= 60;
|
|
let (dx, dw) = if wide {
|
|
let lw = 22.0 * CW();
|
|
let n_vis = (((bottom - top) / (CH() * 1.5)).floor() as usize).max(1);
|
|
let first = sel.saturating_sub(n_vis.saturating_sub(1)).min(list.len().saturating_sub(n_vis));
|
|
for (k, e) in list.iter().enumerate().skip(first).take(n_vis) {
|
|
let r = Rect::new(x + CW() * 2.0, top + (k - first) as f32 * CH() * 1.5, lw, CH() * 1.4);
|
|
let hot = r.contains(vec2(mx, my));
|
|
if k == sel || hot {
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if k == sel { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.18, 0.12, 0.09, 1.0) });
|
|
}
|
|
let name: String = entry_name(*e).chars().take(21).collect();
|
|
gfx.text(&name, r.x + CW() * 0.5, r.y + CH() * 0.2, S(), if k == sel { FRAME_HI } else { TXT });
|
|
self.buttons.push((r, Btn::BestPick(k)));
|
|
}
|
|
(x + lw + CW() * 4.0, (cols as f32 - 28.0) * CW())
|
|
} else {
|
|
let r_prev = Rect::new(x + CW() * 2.0, top, CW() * 3.0, CH() * 2.0);
|
|
let r_next = Rect::new(x + (cols - 5) as f32 * CW(), top, CW() * 3.0, CH() * 2.0);
|
|
button(gfx, r_prev, "\u{25c4}", r_prev.contains(vec2(mx, my)), FRAME);
|
|
button(gfx, r_next, "\u{25ba}", r_next.contains(vec2(mx, my)), FRAME);
|
|
self.buttons.push((r_prev, Btn::BestSel(-1)));
|
|
self.buttons.push((r_next, Btn::BestSel(1)));
|
|
let name = format!("{} ({}/{})", entry_name(entry), sel + 1, list.len());
|
|
gfx.text(&name, grid_center(sw, gfx.measure(&name, S())), top + CH() * 0.5, S(), FRAME_HI);
|
|
(x + CW() * 2.0, (cols as f32 - 4.0) * CW())
|
|
};
|
|
let dy = if wide { top } else { top + CH() * 3.0 };
|
|
self.draw_entry(gfx, entry, level, dx, dy, dw, bottom);
|
|
// back
|
|
let by = y + (rows - 3) as f32 * CH();
|
|
let back = tr("menu.back");
|
|
let br = Rect::new(x + CW() * 2.0, by, gfx.measure(&back, S()) + CW() * 6.0, CH() * 2.0);
|
|
button(gfx, br, &back, br.contains(vec2(mx, my)), FRAME);
|
|
self.buttons.push((br, Btn::Back));
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn draw_entry(&mut self, gfx: &Gfx, entry: Entry, level: u32, x: f32, y: f32, w: f32, bottom: f32) {
|
|
let (mx, my) = mouse();
|
|
let k = (S() * 6.0).floor().max(3.0); // pixel size of the big picture
|
|
let pic = 12.0 * k;
|
|
draw_rectangle(x, y, pic + 2.0 * k, pic + 2.0 * k, Color::new(0.1, 0.07, 0.07, 1.0));
|
|
draw_rectangle_lines(x, y, pic + 2.0 * k, pic + 2.0 * k, 2.0, FRAME);
|
|
let show_level = match entry {
|
|
Entry::Rank(l) => l,
|
|
_ => level,
|
|
};
|
|
match entry {
|
|
Entry::Unit(kind, _) => draw_sprite(gfx, kind, show_level, x + k, y + k, pic),
|
|
Entry::Rank(l) => draw_sprite(gfx, EK::Ogre, l, x + k, y + k, pic),
|
|
Entry::Building(i) => {
|
|
let icon = tool_info(TOOLS[i]).icon;
|
|
draw_texture_ex(&gfx.atlas, x + k * 2.0, y + k * 2.0, WHITE, DrawTextureParams { source: Some(Gfx::cell(icon)), dest_size: Some(vec2(pic - 2.0 * k, pic - 2.0 * k)), ..Default::default() });
|
|
}
|
|
}
|
|
// level selector with the rank badge under the picture
|
|
let ly = y + pic + 3.0 * k;
|
|
let maxl = max_level(entry);
|
|
if maxl > 1 {
|
|
let r_dn = Rect::new(x, ly, CW() * 2.5, CH() * 2.0);
|
|
let r_up = Rect::new(x + pic + 2.0 * k - CW() * 2.5, ly, CW() * 2.5, CH() * 2.0);
|
|
button(gfx, r_dn, "-", r_dn.contains(vec2(mx, my)), FRAME);
|
|
button(gfx, r_up, "+", r_up.contains(vec2(mx, my)), FRAME);
|
|
self.buttons.push((r_dn, Btn::BestLevel(-1)));
|
|
self.buttons.push((r_up, Btn::BestLevel(1)));
|
|
let lv = trf("best.level", &[level.to_string()]);
|
|
gfx.text(&lv, x + (pic + 2.0 * k - gfx.measure(&lv, S())) * 0.5, ly + CH() * 0.5, S(), GOLD);
|
|
}
|
|
if let Entry::Unit(..) | Entry::Rank(_) = entry {
|
|
let bk = (S() * 2.0).max(2.0);
|
|
gfx.badge(x + k + bk, y + k + bk, show_level, bk);
|
|
}
|
|
// facts beside the picture, the text below
|
|
let fx = x + pic + 4.0 * k;
|
|
let fw = ((w - (fx - x)) / CW()).max(10.0) as usize;
|
|
let mut facts: Vec<(String, Color)> = Vec::new();
|
|
let text_key;
|
|
match entry {
|
|
Entry::Unit(kind, cave) => {
|
|
let s = st(kind);
|
|
let l = (level.max(1) - 1) as f32;
|
|
let hp = s.hp * (1.0 + 0.15 * l);
|
|
let atk = s.atk * (1.0 + 0.12 * l);
|
|
facts.push((entry_name(entry), FRAME_HI));
|
|
facts.push((trf("info.rank", &[tr(&format!("rank.{}", level.clamp(1, 10)))]), GOLD));
|
|
facts.push((trf("best.hp", &[(hp as i32).to_string()]), GREEN_C));
|
|
facts.push((trf("best.attack", &[format!("{:.0}", atk), format!("{:.1}", s.interval), format!("{:.0}", atk / s.interval)]), RED_C));
|
|
let range = if s.range > 1.5 { trf("best.range", &[format!("{:.1}", s.range)]) } else { tr("best.melee") };
|
|
facts.push((range, TXT));
|
|
facts.push((trf("best.speed", &[format!("{:.1}", s.speed)]), TXT));
|
|
let id = unit_id(kind, cave);
|
|
if cave {
|
|
facts.push((tr("best.neutral"), Color::new(0.9, 0.6, 0.3, 1.0)));
|
|
} else if HUMANS.contains(&kind) {
|
|
facts.push((trf("best.bounty", &[s.bounty.to_string()]), GOLD));
|
|
} else if s.wage > 0 {
|
|
facts.push((trf("best.wage", &[((s.wage as f32 * (1.0 + 0.1 * l)) as i32).to_string()]), GOLD));
|
|
}
|
|
if !HUMANS.contains(&kind) && !cave {
|
|
facts.push((trf("best.lure", &[tr(&format!("best.{}.lure", id))]), BLUE_C));
|
|
// personality: favourite room, hatreds, learned ability
|
|
let p = crate::personality::persona(kind);
|
|
if let Some(r) = crate::game::room_index(p.likes) {
|
|
facts.push((trf("best.likes", &[tr(crate::game::ROOM_NAMES[r])]), GREEN_C));
|
|
}
|
|
if !p.hates.is_empty() {
|
|
let names: Vec<String> = p.hates.iter().map(|&h| tr(st(h).name)).collect();
|
|
facts.push((trf("best.hates", &[names.join(", ")]), RED_C));
|
|
}
|
|
if let Some((lv, a)) = p.ability {
|
|
let line = trf("best.ability", &[lv.to_string(), tr(&format!("ability.{}.name", a)), tr(&format!("ability.{}.desc", a))]);
|
|
facts.push((line, if level >= lv { GOLD } else { DIM }));
|
|
}
|
|
}
|
|
text_key = format!("best.{}.text", id);
|
|
}
|
|
Entry::Building(i) => {
|
|
let ti = tool_info(TOOLS[i]);
|
|
facts.push((ti.name.clone(), FRAME_HI));
|
|
if let Some(kind) = tower_kind(i) {
|
|
let s = tower_stats(kind);
|
|
let l = (level.max(1) - 1) as usize;
|
|
facts.push((trf("best.hp", &[((s.hp * [1.0, 1.4, 1.9][l]) as i32).to_string()]), GREEN_C));
|
|
facts.push((trf("best.attack", &[format!("{:.0}", s.dmg * [1.0, 1.5, 2.2][l]), format!("{:.1}", s.interval), format!("{:.0}", s.dmg * [1.0, 1.5, 2.2][l] / s.interval)]), RED_C));
|
|
facts.push((trf("best.range", &[format!("{:.1}", s.range + 0.75 * l as f32)]), TXT));
|
|
}
|
|
let cost = match TOOLS[i] {
|
|
crate::ui::Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
|
|
t => tool_cost_static(t),
|
|
};
|
|
if !cost.is_empty() {
|
|
facts.push((trf("ui.cost", &[cost]), GOLD));
|
|
}
|
|
for l in wrap(&ti.desc, fw) {
|
|
facts.push((l, DIM));
|
|
}
|
|
let id = match TOOLS[i] {
|
|
crate::ui::Tool::Room(r) => crate::game::ROOM_NAMES[r].trim_start_matches("room.").to_string(),
|
|
crate::ui::Tool::Tower(k) => crate::game::tower_name(k).trim_start_matches("tower.").to_string(),
|
|
crate::ui::Tool::Bridge => "bridge".into(),
|
|
crate::ui::Tool::Drawbridge => "drawbridge".into(),
|
|
crate::ui::Tool::Wall => "wall".into(),
|
|
crate::ui::Tool::Trap(k) => k.id().into(),
|
|
_ => "steel".into(),
|
|
};
|
|
text_key = format!("best.b.{}.text", id);
|
|
}
|
|
Entry::Rank(l) => {
|
|
facts.push((trf("best.rank_title", &[l.to_string(), tr(&format!("rank.{}", l))]), FRAME_HI));
|
|
facts.push((trf("best.rank_bonus", &[format!("{}", 15 * (l - 1)), format!("{}", 12 * (l - 1))]), GREEN_C));
|
|
facts.push((tr(&format!("best.badge.{}", match l { 1 => 0, 2..=4 => 1, 5..=7 => 2, _ => 3 })), GOLD));
|
|
text_key = format!("best.gear.{}", crate::gfx::gear_tier(l));
|
|
}
|
|
}
|
|
let mut fy = y;
|
|
for (l, c) in facts.iter() {
|
|
for part in wrap(l, fw) {
|
|
if fy + CH() > bottom {
|
|
break;
|
|
}
|
|
gfx.text(&part, fx, fy, S(), *c);
|
|
fy += CH();
|
|
}
|
|
}
|
|
let mut ty = (ly + CH() * 3.0).max(fy + CH());
|
|
let tw = (w / CW()).max(10.0) as usize;
|
|
for para in tr(&text_key).split(" | ") {
|
|
for l in wrap(para, tw) {
|
|
if ty + CH() > bottom {
|
|
return;
|
|
}
|
|
gfx.text(&l, x, ty, S(), TXT);
|
|
ty += CH();
|
|
}
|
|
ty += CH() * 0.5;
|
|
}
|
|
}
|
|
}
|
|
|
|
// a unit sprite with the gear of a level, scaled up
|
|
fn draw_sprite(gfx: &Gfx, kind: EK, level: u32, x: f32, y: f32, size: f32) {
|
|
let src = Gfx::sprite_cell(kind, false, level);
|
|
draw_texture_ex(&gfx.sprites, x, y, WHITE, DrawTextureParams { source: Some(src), dest_size: Some(vec2(size, size)), ..Default::default() });
|
|
}
|