Bestiary in the title and pause menu; attacks look bigger with the unit level

- tabs: creatures, humans (incl. cave dwarves), dungeon buildings and towers, ranks
- every unit at every level: sprite with gear, rank badge, health, attack, range, speed, wage or bounty, what attracts it
- Crookfinger's texts: loving about the creatures, disgusted about the humans (EN/DE)
- projectiles, hit sparks and splashes grow with the attacker's level, with halos from Lv 5 and glow at Lv 9+

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 15:32:18 +01:00
1 parent 9a9f04ae1f
commit 1016ee5d6c
8 files changed
+514 -12

No files matched your search

+18 -4
View File
@@ -375,6 +375,7 @@ pub struct Fx {
pub col: u32,
pub h: f32, // height of the start point (3D)
pub h2: f32, // height of the end point (projectiles)
pub size: f32, // 1 = normal; attacks of higher levels look bigger
}
#[derive(Serialize, Deserialize)]
@@ -916,7 +917,7 @@ impl Game {
pub fn fx(&mut self, kind: u8, x: f32, y: f32, x2: f32, y2: f32, max: f32, col: u32) {
let h = if kind == 0 { 0.45 } else { 0.4 };
if self.fx.len() < 400 {
self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45 });
self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45, size: 1.0 });
}
}
@@ -924,14 +925,21 @@ impl Game {
#[allow(clippy::too_many_arguments)]
pub fn shot(&mut self, x: f32, y: f32, h: f32, x2: f32, y2: f32, h2: f32, max: f32, col: u32) {
if self.fx.len() < 400 {
self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2 });
self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2, size: 1.0 });
}
}
// the last effect gets bigger (attacks of experienced units)
fn grow_last_fx(&mut self, size: f32) {
if let Some(f) = self.fx.last_mut() {
f.size = size;
}
}
// effect at a given height
pub fn fx_at(&mut self, kind: u8, x: f32, y: f32, h: f32, max: f32, col: u32) {
if self.fx.len() < 400 {
self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h });
self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h, size: 1.0 });
}
}
@@ -1806,6 +1814,8 @@ impl Game {
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
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)
let lv = 1.0 + 0.1 * (e.level.max(1) - 1) as f32;
if s.range > 1.5 {
let col = match e.kind {
EK::Mage => 0xffff44cc,
@@ -1818,9 +1828,11 @@ impl Game {
// flyers shoot from the air
let h = if flies(e.kind) { 0.8 } else { 0.45 };
self.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col);
self.grow_last_fx(lv);
self.sfx(Sfx::Shoot, e.x, e.y);
} else {
self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff);
self.fx(2, tx, ty, 0.0, 0.0, 0.25, if e.level >= 9 { 0xff60e0ff } else { 0xffffffff });
self.grow_last_fx(lv);
self.sfx(Sfx::MeleeHit, tx, ty);
}
e.xp += dmg * 0.6;
@@ -1836,6 +1848,7 @@ impl Game {
if let Some(o) = second {
let (ox, oy) = (self.ents[o].x, self.ents[o].y);
self.fx(2, ox, oy, 0.0, 0.0, 0.25, 0xff40a0ff);
self.grow_last_fx(lv);
self.ents[o].hp -= dmg * 0.5;
self.ents[o].flash = 0.15;
if self.ents[o].hp <= 0.0 {
@@ -1846,6 +1859,7 @@ impl Game {
if e.kind == EK::Priestess || e.kind == EK::Wyrm {
// fireball / fire breath: half damage to enemies around the target
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
self.grow_last_fx(lv);
let me = e.side();
let splash: Vec<usize> = self
.ents
+2
View File
@@ -994,6 +994,7 @@ async fn main() {
| Some(Page::Briefing { ingame: false, .. })
| Some(Page::Login)
| Some(Page::Account)
| Some(Page::Bestiary { from_title: true, .. })
);
music.update(&app.game, on_title, (app.cam.tx, app.cam.tz));
for m in app.game.msgs.drain(..) {
@@ -1049,6 +1050,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
| Some(Page::Briefing { ingame: false, .. })
| Some(Page::Login)
| Some(Page::Account)
| Some(Page::Bestiary { from_title: true, .. })
);
if !title_screen {
let info = if over_ui || (touch && inp.inspect.is_none()) {
+21
View File
@@ -12,6 +12,8 @@ use crate::settings::{all_actions, key_name, Action, Settings};
use crate::ui::{grid_center, grid_center_y, mouse, sh, sw, tool_info, wrap, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, SMALL, TITLE, TOOLS, TXT};
use macroquad::prelude::*;
mod bestiary;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Page {
Title,
@@ -24,6 +26,7 @@ pub enum Page {
Briefing { mission: usize, ingame: bool }, // story and goals of a mission
Login, // login to the savegame service (before the title)
Account, // password, logout, users (admin)
Bestiary { tab: usize, sel: usize, level: u32, from_title: bool }, // units, buildings, ranks
}
pub const TABS: [&str; 5] = ["set.tab.sound", "set.tab.camera", "set.tab.graphics", "set.tab.game", "set.tab.keys"];
@@ -214,6 +217,11 @@ enum Btn {
Logout,
CreateUser,
DeleteUser(usize),
Bestiary,
BestTab(usize),
BestSel(i32),
BestPick(usize),
BestLevel(i32),
}
pub const REPO_URL: &str = "https://git.familie-zink.org/zink/dungeoncraft";
@@ -325,6 +333,8 @@ impl Menu {
Some(Page::Campaign) => Some(Page::Title),
Some(Page::Login) => Some(Page::Login),
Some(Page::Account) => Some(Page::Title),
Some(Page::Bestiary { from_title: true, .. }) => Some(Page::Title),
Some(Page::Bestiary { from_title: false, .. }) => Some(Page::Pause),
Some(Page::Briefing { ingame: true, .. }) => Some(Page::Pause),
Some(Page::Briefing { ingame: false, .. }) => Some(Page::Campaign),
Some(Page::Pause) => None,
@@ -547,6 +557,14 @@ impl Menu {
}
MenuAction::Consumed
}
Some(Btn::Bestiary) => {
self.open_bestiary(from_title);
MenuAction::Consumed
}
Some(b @ (Btn::BestTab(_) | Btn::BestSel(_) | Btn::BestPick(_) | Btn::BestLevel(_))) => {
self.bestiary_click(b);
MenuAction::Consumed
}
Some(Btn::FileSave) => MenuAction::FileSave,
Some(Btn::FileLoad) => MenuAction::FileLoad,
}
@@ -568,6 +586,7 @@ impl Menu {
Page::Login => self.draw_login(gfx, cloud),
Page::Account => self.draw_account(gfx, cloud),
Page::Briefing { mission, ingame } => self.draw_briefing(gfx, mission, ingame),
Page::Bestiary { tab, sel, level, .. } => self.draw_bestiary(gfx, tab, sel, level),
Page::KeyCapture { action, .. } => {
self.draw_settings(gfx, settings, 4);
self.buttons.clear();
@@ -629,6 +648,7 @@ impl Menu {
items.push((Btn::Account, trf("menu.account", &[s.name.clone()])));
items.push((Btn::Standalone, tr("menu.standalone")));
}
items.push((Btn::Bestiary, tr("menu.bestiary")));
items.push((Btn::Help, tr("menu.help")));
let w = 26.0 * CW();
let y = ty + GH * big + CH() * 4.0;
@@ -725,6 +745,7 @@ impl Menu {
(Btn::Save, tr("menu.save")),
(Btn::Load, tr("menu.load")),
(Btn::Settings, tr("menu.settings")),
(Btn::Bestiary, tr("menu.bestiary")),
(Btn::Help, tr("menu.help")),
(Btn::MainMenu, tr("menu.main")),
]);
+286
View File
@@ -0,0 +1,286 @@
// 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; 12] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
const HUMANS: [EK; 5] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin];
// buildings: indices into ui::TOOLS
const BUILDINGS: [usize; 14] = [2, 3, 4, 5, 6, 12, 13, 18, 17, 19, 14, 15, 16, 20];
#[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);
}
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));
}
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(),
_ => "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() });
}
+16 -8
View File
@@ -1692,9 +1692,13 @@ fn draw_fx(f: &Fx, t: f32) {
let from = vec3(f.x, f.h, f.y);
let to = vec3(f.x2, f.h2, f.y2);
let head = from.lerp(to, p);
let tail = from.lerp(to, (p - 0.35).max(0.0));
line3(tail, head, 0.06, a(0.8));
draw_sphere(head, 0.08, None, c);
let tail = from.lerp(to, (p - 0.35 * f.size.min(1.5)).max(0.0));
line3(tail, head, 0.06 * f.size, a(0.8));
draw_sphere(head, 0.08 * f.size, None, c);
if f.size > 1.35 {
// experienced shooters: a glowing halo around the projectile
draw_sphere(head, 0.16 * f.size, None, a(0.25));
}
}
1 => {
let mut prev = vec3(f.x, 7.0, f.y);
@@ -1708,13 +1712,17 @@ fn draw_fx(f: &Fx, t: f32) {
disc(vec3(f.x, 0.05, f.y), 1.6 * (0.5 + p), Color::new(1.0, 1.0, 0.7, 0.35 * (1.0 - p)), 24);
}
2 => {
let r = 0.1 + 0.3 * p;
for k in 0..6 {
let ang = k as f32 * 1.047;
line3(at, at + vec3(ang.cos() * r, (k as f32 - 2.5) * 0.06, ang.sin() * r), 0.04, a(1.0 - p));
let r = (0.1 + 0.3 * p) * f.size;
let n = if f.size > 1.35 { 10 } else { 6 };
for k in 0..n {
let ang = k as f32 * std::f32::consts::TAU / n as f32;
line3(at, at + vec3(ang.cos() * r, (k as f32 - n as f32 * 0.5 + 0.5) * 0.06 * f.size, ang.sin() * r), 0.04 * f.size, a(1.0 - p));
}
if f.size > 1.6 {
disc(vec3(f.x, 0.03, f.y), r * 1.3, a(0.3 * (1.0 - p)), 12);
}
}
3 => draw_sphere(vec3(f.x, 0.3, f.y), 0.2 + 0.5 * p, None, a(0.4 * (1.0 - p))),
3 => draw_sphere(vec3(f.x, 0.3, f.y), (0.2 + 0.5 * p) * f.size, None, a(0.4 * (1.0 - p))),
4 => {
for k in 0..3 {
let o = vec3((k as f32 - 1.0) * 0.2, 0.3 + p * 0.9 + k as f32 * 0.1, 0.0);
+21
View File
@@ -223,6 +223,27 @@ pub fn tool_info(t: Tool) -> ToolInfo {
ToolInfo { label: tr(&format!("tool.{}.label", id)), name, desc: tr(&format!("tool.{}.desc", id)), icon }
}
// costs that do not depend on the game (bestiary)
pub fn tool_cost_static(t: Tool) -> String {
match t {
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
let s = tower_stats(TowerKind::Rune);
trf("best.cost_rune", &[s.cost[1].to_string(), s.cost[2].to_string()])
}
Tool::Tower(k) => {
let s = tower_stats(k);
let mut c = trf("cost.tower", &[s.cost[0].to_string(), s.cost[1].to_string(), s.cost[2].to_string()]);
if s.mana > 0.0 {
c += &trf("cost.per_shot", &[(s.mana as i32).to_string()]);
}
c
}
_ => String::new(),
}
}
pub fn tool_cost(t: Tool, g: &Game) -> String {
match t {
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),