Hall of the fallen: kills and causes of death, in the game and over all games in the profile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
065fe80218
commit
c52fc4c4ae
28 files changed
+534
-5
No files matched your search
+28
-1
@@ -63,8 +63,13 @@ pub struct Profile {
|
||||
pub totals: BTreeMap<String, f64>,
|
||||
#[serde(default)]
|
||||
pub unlocked: BTreeMap<String, u64>, // achievement id ("heroes.2", "flawless") -> unix time
|
||||
#[serde(default)]
|
||||
pub hall: Vec<crate::names::Fallen>, // the hall of the fallen over all games (newest last)
|
||||
}
|
||||
|
||||
// the hall of the fallen keeps this many
|
||||
pub const HALL_MAX: usize = 200;
|
||||
|
||||
impl Profile {
|
||||
pub fn total(&self, id: &str) -> f64 {
|
||||
self.totals.get(id).copied().unwrap_or(0.0)
|
||||
@@ -80,6 +85,15 @@ impl Profile {
|
||||
let e = self.unlocked.entry(k.clone()).or_insert(*t);
|
||||
*e = (*e).min(*t);
|
||||
}
|
||||
// the fallen of both, once each, in the order they fell
|
||||
for f in &other.hall {
|
||||
if !self.hall.iter().any(|h| h.name == f.name && h.kind == f.kind && h.when == f.when) {
|
||||
self.hall.push(f.clone());
|
||||
}
|
||||
}
|
||||
self.hall.sort_by_key(|h| h.when);
|
||||
let extra = self.hall.len().saturating_sub(HALL_MAX);
|
||||
self.hall.drain(..extra);
|
||||
}
|
||||
|
||||
// unlocks what the totals have reached; returns the new ones
|
||||
@@ -117,6 +131,7 @@ pub struct Context {
|
||||
pub struct Tracker {
|
||||
pub profile: Profile,
|
||||
flushed: Vec<f32>,
|
||||
hall_seen: usize, // entries of the game's hall already in the profile
|
||||
last_time: f32,
|
||||
ended: bool,
|
||||
t: f32,
|
||||
@@ -126,12 +141,13 @@ pub struct Tracker {
|
||||
|
||||
impl Tracker {
|
||||
pub fn new(profile: Profile) -> Tracker {
|
||||
Tracker { profile, flushed: vec![0.0; k::COUNTERS], last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
|
||||
Tracker { profile, flushed: vec![0.0; k::COUNTERS], hall_seen: 0, last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
|
||||
}
|
||||
|
||||
// a different game started (new, loaded): nothing of it has been counted yet
|
||||
fn restart(&mut self, g: &Game) {
|
||||
self.flushed = (0..k::COUNTERS).map(|i| g.stats.get(i)).collect();
|
||||
self.hall_seen = g.hall.len();
|
||||
self.ended = g.state != 0;
|
||||
}
|
||||
|
||||
@@ -154,6 +170,17 @@ impl Tracker {
|
||||
let played = self.t;
|
||||
self.t = 0.0;
|
||||
let mut new = Vec::new();
|
||||
// the newly fallen go into the profile's hall
|
||||
if g.hall.len() > self.hall_seen {
|
||||
for f in &g.hall[self.hall_seen..] {
|
||||
self.profile.hall.push(crate::names::Fallen { when: now, ..f.clone() });
|
||||
}
|
||||
self.hall_seen = g.hall.len();
|
||||
let extra = self.profile.hall.len().saturating_sub(HALL_MAX);
|
||||
self.profile.hall.drain(..extra);
|
||||
self.dirty = true;
|
||||
self.upload = true;
|
||||
}
|
||||
// the counters of the game
|
||||
for i in 0..k::COUNTERS {
|
||||
let cur = g.stats.get(i);
|
||||
|
||||
+25
@@ -303,6 +303,10 @@ pub struct Ent {
|
||||
#[serde(default)]
|
||||
pub name: u32, // seed of the creature's name (names.rs), 0: none (heroes, siege engines)
|
||||
#[serde(default)]
|
||||
pub kills: u32, // heroes this creature has slain (names.rs: the hall of the fallen)
|
||||
#[serde(default)]
|
||||
pub bane: u8, // what hurt it last: sprite index + 1 of a unit kind, or names::BANE_*
|
||||
#[serde(default)]
|
||||
pub ab_cd: f32, // cooldown of the learned ability
|
||||
#[serde(default)]
|
||||
pub mood_t: f32, // seconds until the next mood check (likes and hatreds)
|
||||
@@ -393,6 +397,8 @@ impl Ent {
|
||||
invis: 0.0,
|
||||
plague: 0.0,
|
||||
name: 0,
|
||||
kills: 0,
|
||||
bane: 0,
|
||||
ab_cd: 0.0,
|
||||
mood_t: 0.0,
|
||||
guard: false,
|
||||
@@ -652,6 +658,8 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub memorials: Vec<crate::names::Memorial>, // stones for fallen creatures (names.rs)
|
||||
#[serde(default)]
|
||||
pub hall: Vec<crate::names::Fallen>, // the local master's fallen of this game (names.rs)
|
||||
#[serde(default)]
|
||||
pub trading: crate::traders::Trading, // the visiting trader (traders.rs)
|
||||
#[serde(default)]
|
||||
pub happenings: Option<crate::events::Events>, // random events (free games and versus), see events.rs
|
||||
@@ -1009,6 +1017,7 @@ impl Game {
|
||||
bridgework: Default::default(),
|
||||
weather: Default::default(),
|
||||
memorials: Vec::new(),
|
||||
hall: Vec::new(),
|
||||
trading: Default::default(),
|
||||
happenings: None,
|
||||
snares: Vec::new(),
|
||||
@@ -2791,6 +2800,11 @@ impl Game {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hit_for_test(&mut self, e: &mut Ent, j: usize) {
|
||||
self.hit(e, j);
|
||||
}
|
||||
|
||||
fn hit(&mut self, e: &mut Ent, j: usize) {
|
||||
// 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 };
|
||||
@@ -2910,6 +2924,10 @@ impl Game {
|
||||
let t = &mut self.ents[j];
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour;
|
||||
t.flash = 0.15;
|
||||
t.bane = crate::gfx::sprite_index(e.kind) as u8 + 1;
|
||||
if t.hp <= 0.0 && t.hero && t.captive == 0 {
|
||||
e.kills += 1;
|
||||
}
|
||||
if t.creature() && !matches!(t.job, Job::Attack(_)) {
|
||||
t.job = Job::Attack(my_id);
|
||||
t.path.clear();
|
||||
@@ -3025,6 +3043,13 @@ impl Game {
|
||||
}
|
||||
self.release_all(id);
|
||||
self.hand.retain(|&h| h != id);
|
||||
// a named creature lost in lava or water is remembered too
|
||||
if !hero {
|
||||
let mut dead = self.ents[j].clone();
|
||||
let lava = self.map.get(x as i32, y as i32).map_or(false, |t| t.river() == Some(Kind::Lava));
|
||||
dead.bane = if lava { crate::names::BANE_LAVA } else { crate::names::BANE_WATER };
|
||||
self.raise_memorial(&dead);
|
||||
}
|
||||
self.fx(3, x, y, 0.0, 0.0, 1.0, 0xff2060ff);
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.5, 0xff30a0ff);
|
||||
self.sfx(Sfx::Death, x, y);
|
||||
|
||||
+10
-1
@@ -141,7 +141,16 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel | Tool::Drawbridge | Tool::Wall, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
let Some(j) = j else { return };
|
||||
let Some(j) = j else {
|
||||
// a memorial stone: the hall of the fallen opens
|
||||
let (gx, gy) = (wx.floor() as i32, wy.floor() as i32);
|
||||
if map::inb(gx, gy) && app.game.memorial_at(map::idx(gx, gy)).is_some() {
|
||||
ui.show_overview = true;
|
||||
ui.ov_fallen = true;
|
||||
ui.ov_scroll = 0;
|
||||
}
|
||||
return;
|
||||
};
|
||||
let (id, hero, dwarf) = (app.game.ents[j].id, app.game.ents[j].hero, app.game.ents[j].dwarf());
|
||||
if dwarf {
|
||||
return;
|
||||
|
||||
@@ -303,6 +303,7 @@ enum Btn {
|
||||
MatchChat,
|
||||
Bestiary,
|
||||
Achievements,
|
||||
AchHall, // achievements page: switch to the hall of the fallen and back
|
||||
Tutorial,
|
||||
BestTab(usize),
|
||||
BestSel(i32),
|
||||
@@ -454,6 +455,7 @@ pub struct Menu {
|
||||
scroll: usize,
|
||||
rows_visible: usize,
|
||||
pub profile: crate::achievements::Profile, // shown on the achievements page
|
||||
ach_hall: bool, // the achievements page shows the hall of the fallen
|
||||
buttons: Vec<(Rect, Btn)>,
|
||||
lang_open: Option<Rect>, // the language dropdown is open below/above this field
|
||||
lang_scroll: usize, // first language shown in the open dropdown
|
||||
@@ -497,6 +499,7 @@ impl Menu {
|
||||
focus: 0,
|
||||
error: None,
|
||||
match_ui: MatchUi::default(),
|
||||
ach_hall: false,
|
||||
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
|
||||
match_size: 128,
|
||||
coop_editor: false,
|
||||
@@ -939,6 +942,11 @@ impl Menu {
|
||||
}
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::AchHall) => {
|
||||
self.ach_hall = !self.ach_hall;
|
||||
self.scroll = 0;
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Achievements) => {
|
||||
self.page = Some(Page::Achievements { from_title });
|
||||
self.scroll = 0;
|
||||
|
||||
@@ -15,7 +15,13 @@ impl Menu {
|
||||
let rows = 40.min((sh / CH()) as i32 - 2);
|
||||
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("menu.achievements"), FRAME_HI);
|
||||
let title = if self.ach_hall { tr("hall.title") } else { tr("menu.achievements") };
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
|
||||
if self.ach_hall {
|
||||
self.draw_hall_page(gfx, x, y, cols, rows);
|
||||
self.ach_buttons(gfx, x, y, rows);
|
||||
return;
|
||||
}
|
||||
let list = all();
|
||||
let done = list.iter().filter(|a| self.profile.unlocked.contains_key(&a.0)).count();
|
||||
let head = trf("ach.count", &[done.to_string(), list.len().to_string(), crate::achievements::fmt_count(self.profile.total("minutes"))]);
|
||||
@@ -57,14 +63,75 @@ impl Menu {
|
||||
}
|
||||
}
|
||||
// scrolling and back
|
||||
let _ = ix;
|
||||
self.ach_buttons(gfx, x, y, rows);
|
||||
}
|
||||
|
||||
// scrolling, the switch between achievements and the hall, back
|
||||
fn ach_buttons(&mut self, gfx: &Gfx, x: f32, y: f32, rows: i32) {
|
||||
let ix = x + CW() * 3.0;
|
||||
let by = y + (rows - 3) as f32 * CH();
|
||||
let (mx, my) = mouse();
|
||||
let bw = 14.0 * CW();
|
||||
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::Back, tr("menu.back"))];
|
||||
let switch = if self.ach_hall { tr("menu.achievements") } else { tr("hall.title") };
|
||||
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::AchHall, switch), (Btn::Back, tr("menu.back"))];
|
||||
let bw = bw.min((sw() - ix - CW() * 4.0) / 4.0 - CW());
|
||||
for (k, (b, label)) in items.iter().enumerate() {
|
||||
let r = Rect::new(ix + k as f32 * (bw + CW()), by, bw, CH() * 2.0);
|
||||
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, *b));
|
||||
}
|
||||
}
|
||||
|
||||
// the hall of the fallen over all games: newest first, two lines each
|
||||
fn draw_hall_page(&mut self, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
|
||||
let hall = &self.profile.hall;
|
||||
gfx.text(&trf("hall.count", &[hall.len().to_string()]), x + CW() * 3.0, y + CH() * 1.6, S(), GOLD);
|
||||
let top = y + CH() * 3.5;
|
||||
let bottom = y + (rows - 4) as f32 * CH();
|
||||
let row_h = CH() * 2.4;
|
||||
self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1);
|
||||
self.scroll = self.scroll.min(hall.len().saturating_sub(self.rows_visible));
|
||||
let ix = x + CW() * 3.0;
|
||||
let maxc = (cols - 6) as usize;
|
||||
if hall.is_empty() {
|
||||
gfx.text(&tr("hall.empty"), ix, top, S(), DIM);
|
||||
}
|
||||
for (k, f) in hall.iter().rev().enumerate().skip(self.scroll).take(self.rows_visible) {
|
||||
let ry = top + (k - self.scroll) as f32 * row_h;
|
||||
let name = format!("{} ({})", crate::names::full_name(f.kind, f.name, f.level), trf("info.level", &[tr(crate::game::st(f.kind).name), f.level.to_string()]));
|
||||
gfx.text(&name.chars().take(maxc).collect::<String>(), ix, ry, S(), FRAME_HI);
|
||||
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
|
||||
let line = format!("{}{} · {}", crate::names::cause(f.bane), kills, date(f.when));
|
||||
gfx.text(&line.chars().take(maxc * 2).collect::<String>(), ix + CW() * 2.0, ry + CH(), SMALL(), DIM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a unix time as a date (year-month-day)
|
||||
fn date(unix: u64) -> String {
|
||||
if unix == 0 {
|
||||
return String::new();
|
||||
}
|
||||
// days since 1970-01-01 to a civil date (Howard Hinnant's algorithm)
|
||||
let z = (unix / 86400) as i64 + 719468;
|
||||
let era = z.div_euclid(146097);
|
||||
let doe = z - era * 146097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
|
||||
format!("{:04}-{:02}-{:02}", y, m, d)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn dates_are_civil() {
|
||||
assert_eq!(super::date(0), "");
|
||||
assert_eq!(super::date(1_798_675_200), "2026-12-31");
|
||||
assert_eq!(super::date(1_709_164_800), "2024-02-29");
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,40 @@ use serde::{Deserialize, Serialize};
|
||||
pub const MEMORIAL_LEVEL: u32 = 3;
|
||||
pub const MAX_MEMORIALS: usize = 24;
|
||||
|
||||
// what killed a creature besides another unit (Ent::bane)
|
||||
pub const BANE_SNARE: u8 = 250;
|
||||
pub const BANE_LIGHTNING: u8 = 251;
|
||||
pub const BANE_LAVA: u8 = 252;
|
||||
pub const BANE_WATER: u8 = 253;
|
||||
|
||||
// an entry of the hall of the fallen (in the game and in the player's profile)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Fallen {
|
||||
pub kind: EK,
|
||||
pub name: u32,
|
||||
pub level: u32,
|
||||
pub kills: u32,
|
||||
pub bane: u8,
|
||||
pub time: f32, // game seconds when it fell
|
||||
#[serde(default)]
|
||||
pub when: u64, // unix time (set when it goes into the profile)
|
||||
}
|
||||
|
||||
// how it fell, in words
|
||||
pub fn cause(bane: u8) -> String {
|
||||
match bane {
|
||||
BANE_SNARE => tr("cause.snare"),
|
||||
BANE_LIGHTNING => tr("cause.lightning"),
|
||||
BANE_LAVA => tr("cause.lava"),
|
||||
BANE_WATER => tr("cause.water"),
|
||||
0 => tr("cause.fell"),
|
||||
k => match crate::gfx::SPRITE_KINDS.get(k as usize - 1) {
|
||||
Some(&kind) => trf("cause.slain", &[tr(st(kind).name)]),
|
||||
None => tr("cause.fell"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Memorial {
|
||||
pub tile: usize,
|
||||
@@ -103,6 +137,10 @@ impl Game {
|
||||
if e.hero || e.name == 0 || e.level < MEMORIAL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
|
||||
return false;
|
||||
}
|
||||
// the hall of the fallen keeps the local master's (also without room for a stone)
|
||||
if e.side == self.local_side {
|
||||
self.hall.push(Fallen { kind: e.kind, name: e.name, level: e.level, kills: e.kills, bane: e.bane, time: self.time, when: 0 });
|
||||
}
|
||||
let owner = Owner::of_side(e.side);
|
||||
let (x, y) = (e.x as i32, e.y as i32);
|
||||
let taken: Vec<usize> = self.memorials.iter().map(|m| m.tile).collect();
|
||||
@@ -168,6 +206,50 @@ mod tests {
|
||||
assert_eq!(g.ents.iter().find(|e| e.id == h).unwrap().name, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hall_remembers_kills_and_how_they_fell() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy + 4..cy + 8 {
|
||||
for x in cx..cx + 6 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
let mut t = crate::achievements::Tracker::new(Default::default());
|
||||
let ctx = crate::achievements::Context { coop: false, editor_test: false };
|
||||
g.time = 1.0;
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_000);
|
||||
let ogre = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 5);
|
||||
let knight = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 5.5, 1);
|
||||
// the ogre slays the knight
|
||||
let (oj, kj) = (g.ents.iter().position(|e| e.id == ogre).unwrap(), g.ents.iter().position(|e| e.id == knight).unwrap());
|
||||
g.ents[kj].hp = 1.0;
|
||||
let mut o = g.ents[oj].clone();
|
||||
g.hit_for_test(&mut o, kj);
|
||||
assert_eq!(o.kills, 1, "a hero slain");
|
||||
g.ents[oj] = o;
|
||||
// a second knight brings the ogre down
|
||||
let k2 = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 6.5, 1);
|
||||
let k2j = g.ents.iter().position(|e| e.id == k2).unwrap();
|
||||
let oj = g.ents.iter().position(|e| e.id == ogre).unwrap();
|
||||
g.ents[oj].hp = 1.0;
|
||||
let mut k = g.ents[k2j].clone();
|
||||
g.hit_for_test(&mut k, oj);
|
||||
assert_eq!(g.hall.len(), 1);
|
||||
let f = &g.hall[0];
|
||||
assert_eq!((f.kind, f.kills), (EK::Ogre, 1));
|
||||
assert_eq!(f.bane as u32, crate::gfx::sprite_index(EK::Knight) + 1);
|
||||
assert!(cause(f.bane).contains(&crate::i18n::tr("unit.knight")));
|
||||
// into the profile, once
|
||||
g.time = 100.0;
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_100);
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_200);
|
||||
assert_eq!(t.profile.hall.len(), 1);
|
||||
assert_eq!(t.profile.hall[0].when, 1_800_000_100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fallen_get_a_memorial_stone() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
|
||||
@@ -193,6 +193,7 @@ impl Game {
|
||||
let e = &mut self.ents[j];
|
||||
e.stun = e.stun.max(SNARE_STUN);
|
||||
e.hp -= SNARE_DAMAGE;
|
||||
e.bane = crate::names::BANE_SNARE;
|
||||
e.flash = 0.2;
|
||||
let (x, y, side, kind, dead) = (e.x, e.y, e.side, e.kind, e.hp <= 0.0);
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.4, 0xff4040ff);
|
||||
|
||||
@@ -461,6 +461,7 @@ enum TBtn {
|
||||
Learn(u8), // spend a point on a branch
|
||||
OvRow(u32),
|
||||
OvScroll(i32),
|
||||
OvFallen, // switch the overview between the living and the fallen
|
||||
GoldTo, // the next ally to receive gold
|
||||
Gold(u32), // send that much gold to him
|
||||
}
|
||||
@@ -491,7 +492,8 @@ pub struct Ui {
|
||||
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,
|
||||
pub ov_scroll: usize,
|
||||
pub ov_fallen: bool, // the overview shows the hall of the fallen
|
||||
ov_rect: Option<Rect>, // where the creature overview is (mouse wheel, swipe)
|
||||
pub show_langsel: bool,
|
||||
pub log: Vec<(Msg, f64)>,
|
||||
@@ -603,6 +605,7 @@ impl Ui {
|
||||
coop_end: None,
|
||||
gold_to: 0,
|
||||
ov_scroll: 0,
|
||||
ov_fallen: false,
|
||||
ov_rect: None,
|
||||
show_langsel: false,
|
||||
log: Vec::new(),
|
||||
@@ -702,6 +705,11 @@ impl Ui {
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Learn(b) => UiAction::LearnTalent(b),
|
||||
TBtn::OvFallen => {
|
||||
self.ov_fallen = !self.ov_fallen;
|
||||
self.ov_scroll = 0;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::OvScroll(d) => {
|
||||
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
||||
UiAction::Consumed
|
||||
@@ -1343,6 +1351,36 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
// the hall of the fallen of this game: name, kind, level, heroes slain, how it fell
|
||||
fn draw_hall(&mut self, g: &Game, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
|
||||
gfx.text(&tr("hall.title"), x + CW() * 2.0, y + CH(), S(), GOLD);
|
||||
let mut lines: Vec<(String, Color)> = Vec::new();
|
||||
for f in g.hall.iter().rev() {
|
||||
lines.push((format!("{:>2} {}", f.level, crate::names::full_name(f.kind, f.name, f.level)), FRAME_HI));
|
||||
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
|
||||
lines.push((format!(" {} · {}{} · {}", tr(st(f.kind).name), crate::names::cause(f.bane), kills, mmss(f.time)), DIM));
|
||||
}
|
||||
if lines.is_empty() {
|
||||
lines.push((tr("hall.empty"), DIM));
|
||||
}
|
||||
let first = y + CH() * 2.6;
|
||||
let visible = (rows - 4).max(1) as usize;
|
||||
self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible));
|
||||
let maxc = (cols - 4) as usize;
|
||||
for (n, (text, col)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) {
|
||||
let t: String = text.chars().take(maxc).collect();
|
||||
gfx.text(&t, x + CW() * 2.0, first + (n - self.ov_scroll) as f32 * CH(), S(), *col);
|
||||
}
|
||||
if lines.len() > visible {
|
||||
let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH() * 2.4, CW() * 2.0, CH() * 1.5);
|
||||
let down = Rect::new(x + (cols - 4) as f32 * CW(), y + (rows - 2) as f32 * CH(), CW() * 2.0, CH() * 1.5);
|
||||
gfx.text("\u{25b2}", up.x + CW() * 0.5, up.y, S(), FRAME_HI);
|
||||
gfx.text("\u{25bc}", down.x + CW() * 0.5, down.y, S(), FRAME_HI);
|
||||
self.touch_btns.push((up, TBtn::OvScroll(-2)));
|
||||
self.touch_btns.push((down, TBtn::OvScroll(2)));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
||||
let (sw, _) = (sw(), sh());
|
||||
let (mx, my) = mouse();
|
||||
@@ -1357,6 +1395,21 @@ impl Ui {
|
||||
let mine: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let wage: f32 = mine.iter().map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
let mood = if mine.is_empty() { 0.0 } else { mine.iter().map(|e| e.happy.clamp(0.0, 1.0)).sum::<f32>() / mine.len() as f32 };
|
||||
// the switch to the hall of the fallen (top right)
|
||||
{
|
||||
let label = if self.ov_fallen { tr("ov.living") } else { trf("ov.fallen", &[g.hall.len().to_string()]) };
|
||||
let bw = gfx.measure(&label, SMALL()) + CW() * 2.0;
|
||||
let r = Rect::new(x + (cols - 3) as f32 * CW() - bw, y + CH() * 0.8, bw, CH() * 1.4);
|
||||
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, FRAME_HI);
|
||||
gfx.text(&label, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), FRAME_HI);
|
||||
self.touch_btns.push((r, TBtn::OvFallen));
|
||||
}
|
||||
if self.ov_fallen {
|
||||
self.draw_hall(g, gfx, x, y, cols, rows);
|
||||
return;
|
||||
}
|
||||
let head = trf("ov.summary", &[mine.len().to_string(), (wage as i32).to_string(), ((mood * 100.0) as i32).to_string()]);
|
||||
gfx.text(&head, x + CW() * 2.0, y + CH(), S(), GOLD);
|
||||
// a match with allies: a row to send them gold (the name switches between the allies)
|
||||
|
||||
@@ -145,6 +145,7 @@ impl Game {
|
||||
let e = &mut self.ents[j];
|
||||
e.hp -= BOLT_DAMAGE;
|
||||
e.flash = 0.3;
|
||||
e.bane = crate::names::BANE_LIGHTNING;
|
||||
let (x, y, side, kind, hero, dead) = (e.x, e.y, e.side, e.kind, e.hero, e.hp <= 0.0);
|
||||
if !hero && side == self.local_side {
|
||||
self.tell_local("msg.lightning_unit", vec![format!("@{}", st(kind).name)], None);
|
||||
|
||||
Reference in new issue
Block a user