From 51814b8ac81b72770726b5fb8c6e2cfc30d915e8 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Wed, 4 Nov 2026 08:02:29 +0100 Subject: [PATCH] Creature overview: a toolbar button opens the list of all creatures grouped by kind (level, mood, wage, job, health) with wages and mood in total; a click moves the camera there Co-Authored-By: Claude Opus 5.5 --- lang/de.lang | 7 ++++ lang/en.lang | 7 ++++ src/ui.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/lang/de.lang b/lang/de.lang index 7009a79..930d4e7 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -916,3 +916,10 @@ hint.order_nobody = Keine Kreatur ist für diesen Befehl frei. msg.order_attack = {0} Kreaturen greifen an! msg.order_guard = {0} Kreaturen beziehen Stellung. msg.orders_cancelled = Wachbefehle aufgehoben. + +# creature overview +cat.overview = Kreaturen +ov.title = Deine Kreaturen +ov.summary = {0} Kreaturen - Lohn {1} Gold pro Zahltag - Laune {2}% +ov.none = Noch keine Kreaturen. Baue ein Quartier und einen Pilzgarten und nimm ein Portal ein. +ov.hint = St., Laune, Lohn, Tätigkeit, Leben - Klick: hingehen diff --git a/lang/en.lang b/lang/en.lang index d32020d..ced5d9c 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -917,3 +917,10 @@ hint.order_nobody = No creature is free for that order. msg.order_attack = {0} creatures attack! msg.order_guard = {0} creatures take their positions. msg.orders_cancelled = Guard orders cancelled. + +# creature overview +cat.overview = Creatures +ov.title = Your creatures +ov.summary = {0} creatures - wages {1} gold per payday - mood {2}% +ov.none = No creatures yet. Build a lair and a mushroom garden and claim a portal. +ov.hint = Lv, mood, wage, job, health - click: go there diff --git a/src/ui.rs b/src/ui.rs index 220ee8d..c89cdff 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -326,6 +326,9 @@ enum TBtn { Cat(usize), Tool(usize), Menu, + Overview, + OvRow(u32), + OvScroll(i32), } pub enum UiAction { @@ -346,6 +349,8 @@ pub enum UiAction { pub struct Ui { pub tool: Tool, pub show_help: bool, + pub show_overview: bool, // list of all creatures + ov_scroll: usize, pub show_langsel: bool, pub log: Vec<(Msg, f64)>, pub rects: Vec, @@ -398,6 +403,8 @@ impl Ui { Ui { tool: Tool::Dig, show_help: true, + show_overview: false, + ov_scroll: 0, show_langsel: false, log: Vec::new(), rects: Vec::new(), @@ -461,6 +468,20 @@ impl Ui { self.open_cat = None; UiAction::OpenMenu } + TBtn::Overview => { + self.open_cat = None; + self.show_overview = !self.show_overview; + self.ov_scroll = 0; + UiAction::Consumed + } + TBtn::OvScroll(d) => { + self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize; + UiAction::Consumed + } + TBtn::OvRow(id) => match g.ents.iter().find(|e| e.id == id) { + Some(e) => UiAction::MinimapJump(e.x, e.y), + None => UiAction::Consumed, + }, TBtn::Tool(i) if !g.tool_allowed(i) => { self.last_locked = Some(i); UiAction::Locked @@ -643,7 +664,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() + 1; + let n = CATS.len() + 2; 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); @@ -672,10 +693,22 @@ impl Ui { } self.touch_btns.push((r, TBtn::Cat(c))); } - // menu button + // creature overview button let bx = x0 + CATS.len() 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 || self.show_overview { 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 self.show_overview { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); + gfx.glyph(0x02, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); + let ol = tr("cat.overview"); + let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize; + 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 + 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)); 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(0xF0, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); @@ -872,6 +905,11 @@ impl Ui { popup_top = py0; } + // ---------------- creature overview + if self.show_overview { + self.draw_overview(g, gfx, tb_y); + } + // ---------------- Tooltip if let Some(i) = hover_tool { let t = TOOLS[i]; @@ -897,6 +935,76 @@ impl Ui { } + // all creatures, grouped by kind: level, mood, wage, job, health; a click jumps there + fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) { + let (sw, _) = (sw(), sh()); + let (mx, my) = mouse(); + let cols = ((sw / CW()) as i32 - 2).min(56); + let top = CH() * 4.0; + let rows = (((bottom - top) / CH()) as i32 - 1).clamp(8, 40); + let x = (sw - cols as f32 * CW() - CW()).max(0.0); + let y = (bottom - rows as f32 * CH()).max(top); + gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("ov.title"), FRAME_HI); + self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH())); + 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::() / mine.len() as f32 }; + 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); + // lines: kind headers and creatures + let mut kinds: Vec = Vec::new(); + for e in &mine { + if !kinds.contains(&e.kind) { + kinds.push(e.kind); + } + } + kinds.sort_by_key(|k| std::cmp::Reverse(mine.iter().filter(|e| e.kind == *k).count())); + let mut lines: Vec<(String, Color, Option, f32)> = Vec::new(); + for k in kinds { + let group: Vec<&&Ent> = mine.iter().filter(|e| e.kind == k).collect(); + lines.push((format!("{} x{}", tr(st(k).name), group.len()), FRAME_HI, None, -1.0)); + let mut group = group; + group.sort_by_key(|e| std::cmp::Reverse(e.level)); + for e in group { + let w = (st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32; + let happy = (e.happy.clamp(0.0, 1.0) * 100.0) as i32; + let job: String = tr(job_name(e)).chars().take(16).collect(); + let line = format!(" {:>2} {:>3}% {:>4}g {:<16}", e.level, happy, w, job); + let col = if e.happy > 0.6 { TXT } else if e.happy > 0.25 { GOLD } else { RED }; + lines.push((line, col, Some(e.id), e.hp / e.max_hp)); + } + } + let first = y + CH() * 3.0; + let visible = (rows - 5).max(1) as usize; + self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible)); + let lw = (cols - 4) as f32 * CW(); + for (n, (text, col, id, hp)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) { + let ly = first + (n - self.ov_scroll) as f32 * CH(); + let r = Rect::new(x + CW() * 2.0, ly, lw, CH()); + if id.is_some() && r.contains(vec2(mx, my)) { + draw_rectangle(r.x, r.y, r.w, r.h, Color::new(0.25, 0.16, 0.08, 1.0)); + } + gfx.text(text, r.x, ly, S(), *col); + if let Some(id) = id { + ascii_bar(gfx, r.x + r.w - CW() * 4.0, ly + (CH() - GH * SMALL()) * 0.5, 6, *hp, if *hp > 0.5 { GREEN } else { RED }, SMALL()); + self.touch_btns.push((r, TBtn::OvRow(*id))); + } + } + if lines.len() > visible { + let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH(), 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(-(visible as i32) / 2))); + self.touch_btns.push((down, TBtn::OvScroll(visible as i32 / 2))); + } + if mine.is_empty() { + gfx.text(&tr("ov.none"), x + CW() * 2.0, first, S(), DIM); + } + let hint = tr("ov.hint"); + gfx.text(&hint, x + CW() * 2.0, y + (rows - 2) as f32 * CH(), SMALL(), DIM); + } + // Dialogs on top of everything (help, language selection, end screen, pause) pub fn draw_dialogs(&mut self, g: &Game, gfx: &Gfx) { let sw = sw();