Switch repo to English, add i18n with EN/DE language files and first-start language selection

- in-game texts moved to lang/en.lang and lang/de.lang (key = text, placeholders)
- game logic emits message keys + args, translated at display time
- localStorage access via own miniquad JS plugin (www/dc_plugin.js)
- code comments, DESIGN.md, CREDITS.md translated; README.md added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 22:53:03 +01:00
1 parent a101f6ddc4
commit fbf6f223fc
18 files changed
+1072 -362

No files matched your search

+181 -121
View File
@@ -1,15 +1,17 @@
// Benutzeroberfläche im Canvas: CP437-Fenster mit DINOBYTE-Schrift.
// In-canvas user interface: CP437 windows with the DINOBYTE bitmap font.
// All texts come from the language files (see i18n.rs).
use crate::game::*;
use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
use crate::map::*;
use crate::render3d::{minimap_color, Cam};
use crate::sound::SoundBank;
use macroquad::prelude::*;
pub const S: f32 = 2.0; // UI-Skalierung der Schrift
pub const CW: f32 = GW * S; // Zeichenbreite in px
pub const CH: f32 = GH * S; // Zeichenhöhe in px
pub const S: f32 = 2.0; // UI font scale
pub const CW: f32 = GW * S; // character width in px
pub const CH: f32 = GH * S; // character height in px
pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0);
pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0);
@@ -52,37 +54,40 @@ pub const TOOLS: [Tool; 12] = [
pub const TOOL_KEYS: [&str; 12] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "X", "V"];
pub struct ToolInfo {
pub label: &'static str,
pub name: &'static str,
pub desc: &'static str,
pub label: String,
pub name: String,
pub desc: String,
pub icon: u32,
}
pub fn tool_info(t: Tool) -> ToolInfo {
let i = |label, name, desc, icon| ToolInfo { label, name, desc, icon };
match t {
Tool::Dig => i("Graben", "Graben", "Erde und Gold markieren. Ziehen für Rechteck; auf markiertem Feld beginnen hebt Markierung auf.", I_PICK),
Tool::Hand => i("Hand", "Hand", "Eigene Einheit aufnehmen (max. 8). Rechtsklick setzt sie auf eigenem Gebiet ab.", I_HAND),
Tool::Room(0) => i("Schatz", "Schatzkammer", "Lagert Gold: +500 Kapazität pro Feld.", T_TREASURY),
Tool::Room(1) => i("Quartier", "Quartier", "Ein Schlafplatz pro Feld. Bestimmt, wie viele Kreaturen kommen.", T_LAIR),
Tool::Room(2) => i("Pilze", "Pilzgarten", "Nahrung für Kreaturen. Ab 6 Feldern kommen Höhlenspinnen.", T_FARM),
Tool::Room(3) => i("Training", "Trainingshalle", "Kreaturen steigen Stufen auf (kostet Gold). Ab 9 Feldern kommen Steinoger.", T_TRAINING),
Tool::Room(4) => i("Bücher", "Bibliothek", "Kreaturen erzeugen Mana. Ab 6 Feldern kommen Schattenmagier.", T_LIBRARY),
Tool::Sell => i("Verkauf", "Verkaufen", "Räume zurück in Boden verwandeln (50 % Erstattung).", I_SELL),
Tool::Flag => i("Flagge", "Angriffsflagge", "Kampfbereite Kreaturen sammeln sich hier. Nahe am Heldentor greifen sie es an. Erneut klicken entfernt sie.", I_FLAG),
Tool::Lightning => i("Blitz", "Blitz", "55 Schaden an Helden im Umkreis.", I_BOLT),
Tool::Heal => i("Heilung", "Heilung", "Heilt eigene Einheiten im Umkreis um 50 %.", I_HEAL),
Tool::Summon => i("Wicht", "Grubenwicht beschwören", "Erschafft einen neuen Grubenwicht auf eigenem Gebiet.", I_SUMMON),
Tool::Room(_) => i("?", "?", "", 0),
}
let (id, icon, room) = match t {
Tool::Dig => ("dig", I_PICK, None),
Tool::Hand => ("hand", I_HAND, None),
Tool::Room(0) => ("treasury", T_TREASURY, Some(0)),
Tool::Room(1) => ("lair", T_LAIR, Some(1)),
Tool::Room(2) => ("farm", T_FARM, Some(2)),
Tool::Room(3) => ("training", T_TRAINING, Some(3)),
Tool::Room(_) => ("library", T_LIBRARY, Some(4)),
Tool::Sell => ("sell", I_SELL, None),
Tool::Flag => ("flag", I_FLAG, None),
Tool::Lightning => ("lightning", I_BOLT, None),
Tool::Heal => ("heal", I_HEAL, None),
Tool::Summon => ("summon", I_SUMMON, None),
};
let name = match room {
Some(r) => tr(ROOM_NAMES[r]),
None => tr(&format!("tool.{}.name", id)),
};
ToolInfo { label: tr(&format!("tool.{}.label", id)), name, desc: tr(&format!("tool.{}.desc", id)), icon }
}
pub fn tool_cost(t: Tool, g: &Game) -> String {
match t {
Tool::Room(r) => format!("{} Gold/Feld", ROOM_COST[r]),
Tool::Lightning => "50 Mana".into(),
Tool::Heal => "60 Mana".into(),
Tool::Summon => format!("{} Mana", g.summon_cost() as i32),
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
Tool::Lightning => trf("cost.mana", &["50".into()]),
Tool::Heal => trf("cost.mana", &["60".into()]),
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
_ => String::new(),
}
}
@@ -93,13 +98,15 @@ pub enum UiAction {
MinimapJump(f32, f32),
CloseHelp,
NewGame,
SelectLanguage(String),
Consumed,
}
pub struct Ui {
pub tool: Tool,
pub show_help: bool,
pub log: Vec<(String, f64)>,
pub show_langsel: bool,
pub log: Vec<(Msg, f64)>,
pub rects: Vec<Rect>,
minimap_img: Image,
minimap_tex: Texture2D,
@@ -107,6 +114,7 @@ pub struct Ui {
minimap_rect: Rect,
tool_rects: Vec<Rect>,
end_button: Rect,
lang_rects: Vec<(Rect, String)>,
}
fn wrap(s: &str, cols: usize) -> Vec<String> {
@@ -148,6 +156,7 @@ impl Ui {
Ui {
tool: Tool::Dig,
show_help: true,
show_langsel: false,
log: Vec::new(),
rects: Vec::new(),
minimap_img: img,
@@ -156,11 +165,12 @@ impl Ui {
minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
tool_rects: Vec::new(),
end_button: Rect::new(0.0, 0.0, 0.0, 0.0),
lang_rects: Vec::new(),
}
}
pub fn over_ui(&self, x: f32, y: f32) -> bool {
self.show_help || self.rects.iter().any(|r| r.contains(vec2(x, y)))
self.show_help || self.show_langsel || self.rects.iter().any(|r| r.contains(vec2(x, y)))
}
pub fn minimap_contains(&self, x: f32, y: f32) -> bool {
@@ -174,6 +184,14 @@ impl Ui {
pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
let p = vec2(x, y);
if self.show_langsel {
for (r, code) in &self.lang_rects {
if r.contains(p) {
return UiAction::SelectLanguage(code.clone());
}
}
return UiAction::Consumed;
}
if g.state != 0 {
if self.end_button.contains(p) {
return UiAction::NewGame;
@@ -204,7 +222,7 @@ impl Ui {
let sh = screen_height();
let (mx, my) = mouse_position();
// ---------------- obere Leiste
// ---------------- top bar
let cols = (sw / CW).ceil() as i32;
gfx.window(0.0, 0.0, cols, 3, S, false, FRAME, BG);
self.rects.push(Rect::new(0.0, 0.0, sw, 3.0 * CH));
@@ -216,31 +234,34 @@ impl Ui {
gfx.text(value, *x, y, S, vc);
*x += gfx.measure(value, S) + CW * 2.0;
};
seg("Gold", GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
gfx.text("Mana", x, y, S, MANA);
x += gfx.measure("Mana", S) + CW * 0.5;
seg(&tr("hud.gold"), GOLD, &format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut x);
let mana = tr("hud.mana");
gfx.text(&mana, x, y, S, MANA);
x += gfx.measure(&mana, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.mana / MANA_MAX, MANA);
x += 70.0 + CW * 0.5;
gfx.text(&format!("{}", g.mana as i32), x, y, S, TXT);
x += CW * 5.0;
gfx.text("Kern", x, y, S, RED);
x += gfx.measure("Kern", S) + CW * 0.5;
let core = tr("hud.core");
gfx.text(&core, x, y, S, RED);
x += gfx.measure(&core, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.core_hp / CORE_HP, RED);
x += 70.0 + CW * 1.5;
gfx.text("Tor", x, y, S, BLUE);
x += gfx.measure("Tor", S) + CW * 0.5;
let gate = tr("hud.gate");
gfx.text(&gate, x, y, S, BLUE);
x += gfx.measure(&gate, S) + CW * 0.5;
bar(x, y + 3.0, 70.0, 10.0, g.gate_hp / GATE_HP, BLUE);
x += 70.0 + CW * 1.5;
let cap = g.counts[1].min(30);
seg("Kreaturen", TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg("Wichte", TXT, &format!("{}", g.worker_count()), TXT, &mut x);
seg(&tr("hud.creatures"), TXT, &format!("{}/{}", g.creature_count(), cap), TXT, &mut x);
seg(&tr("hud.workers"), TXT, &format!("{}", g.worker_count()), TXT, &mut x);
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
seg(&format!("Welle {}", g.wave + 1), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg("Armee", TXT, &format!("{}", g.army_value() as i32), TXT, &mut x);
let vol = if snd.muted { "aus".to_string() } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg(&trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, &mmss(g.wave_timer), wave_col, &mut x);
seg(&tr("hud.army"), TXT, &format!("{}", g.army_value() as i32), TXT, &mut x);
let vol = if snd.muted { tr("hud.off") } else { format!("{}%", (snd.volume * 100.0).round() as i32) };
seg("♪", DIM, &vol, DIM, &mut x);
// ---------------- Werkzeugleiste
// ---------------- toolbar
let tb_rows = 5;
let tb_y = sh - tb_rows as f32 * CH;
gfx.window(0.0, tb_y, cols, tb_rows, S, false, FRAME, BG);
@@ -263,13 +284,13 @@ impl Ui {
let ti = tool_info(t);
gfx.draw_cell(ti.icon, bx + (bw - 32.0) * 0.5, by + 4.0, 32.0, 32.0, WHITE);
gfx.text(TOOL_KEYS[i], bx + 3.0, by + 3.0, 1.0, if sel { FRAME_HI } else { DIM });
let lw = gfx.measure(ti.label, 1.0);
gfx.text(ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT });
let lw = gfx.measure(&ti.label, 1.0);
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0, 1.0, if sel { FRAME_HI } else { TXT });
}
// Hand
// hand contents
let hx = CW + TOOLS.len() as f32 * (bw + 4.0) + CW;
if hx + 120.0 < sw - 200.0 {
gfx.text("Hand", hx, tb_y + CH, S, TXT);
gfx.text(&tr("hud.hand"), hx, tb_y + CH, S, TXT);
for (k, id) in g.hand.iter().enumerate() {
if let Some(e) = g.ents.iter().find(|e| e.id == *id) {
let sx = hx + (k % 4) as f32 * 26.0;
@@ -284,18 +305,18 @@ impl Ui {
}
}
if g.hand.is_empty() {
gfx.text("leer", hx, tb_y + CH * 2.5, 1.0, DIM);
gfx.text(&tr("hud.empty"), hx, tb_y + CH * 2.5, 1.0, DIM);
}
}
// ---------------- Minikarte
// ---------------- minimap
let mm_cols = 16;
let mm_rows = 12;
let mm_w = mm_cols as f32 * CW;
let mm_h = mm_rows as f32 * CH;
let mm_x = sw - mm_w;
let mm_y = tb_y - mm_h;
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S, false, FRAME, BG, "Karte", FRAME_HI);
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S, false, FRAME, BG, &tr("ui.map"), FRAME_HI);
self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
if now - self.minimap_t > 0.25 {
self.minimap_t = now;
@@ -335,7 +356,7 @@ impl Ui {
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
}
// ---------------- Infofeld
// ---------------- info panel
if !info.is_empty() {
let icols = 30;
let mut lines: Vec<(String, Color)> = Vec::new();
@@ -353,14 +374,14 @@ impl Ui {
}
}
// ---------------- Meldungen
let recent: Vec<&(String, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
// ---------------- messages (translated at display time, so a language switch applies at once)
let recent: Vec<&(Msg, f64)> = self.log.iter().filter(|(_, t)| now - t < 14.0).collect();
if !recent.is_empty() {
let mcols = (((sw - mm_w) / CW) as i32 - 2).clamp(20, 70);
let mut lines: Vec<(String, f32)> = Vec::new();
for (s, t) in recent.iter().rev().take(5).rev() {
for (m, t) in recent.iter().rev().take(5).rev() {
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
for l in wrap(s, mcols as usize - 4) {
for l in wrap(&trf(m.key, &m.args), mcols as usize - 4) {
lines.push((l, a));
}
}
@@ -381,23 +402,25 @@ impl Ui {
let t = TOOLS[i];
let ti = tool_info(t);
let tcols = 34;
let mut lines: Vec<(String, Color)> = wrap(ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
let mut lines: Vec<(String, Color)> = wrap(&ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
let cost = tool_cost(t, g);
if !cost.is_empty() {
lines.push((format!("Kosten: {}", cost), GOLD));
lines.push((trf("ui.cost", &[cost]), GOLD));
}
lines.push((format!("Taste: {}", TOOL_KEYS[i]), DIM));
lines.push((trf("ui.key", &[TOOL_KEYS[i].to_string()]), DIM));
let rows = lines.len() as i32 + 2;
let tx = (mx - tcols as f32 * CW * 0.5).clamp(0.0, sw - tcols as f32 * CW);
let ty = tb_y - rows as f32 * CH - 4.0;
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, ti.name, FRAME_HI);
gfx.window_titled(tx, ty, tcols as i32, rows, S, false, FRAME_HI, BG, &ti.name, FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, tx + CW * 2.0, ty + CH * (k as f32 + 1.0), S, *c);
}
}
// ---------------- Dialoge
if g.state != 0 {
// ---------------- dialogs
if self.show_langsel {
self.draw_langsel(gfx);
} else if g.state != 0 {
self.draw_end(g, gfx);
} else if self.show_help {
self.draw_help(gfx);
@@ -406,36 +429,37 @@ impl Ui {
let x = (sw - w as f32 * CW) * 0.5;
let y = sh * 0.35;
gfx.window(x, y, w, 5, S, true, FRAME_HI, BG);
let t = "PAUSE";
gfx.text(t, (sw - gfx.measure(t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI);
let t2 = "P: weiter";
gfx.text(t2, (sw - gfx.measure(t2, S)) * 0.5, y + CH * 3.0, S, DIM);
let t = tr("ui.pause");
gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH, 3.0, FRAME_HI);
let t2 = tr("ui.pause_hint");
gfx.text(&t2, (sw - gfx.measure(&t2, S)) * 0.5, y + CH * 3.0, S, DIM);
}
}
fn draw_help(&mut self, gfx: &Gfx) {
let lines: &[(&str, Color)] = &[
("Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern.", GOLD),
let keys: &[(&str, Color)] = &[
("help.goal", GOLD),
("", TXT),
("Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen)", TXT),
("Rechtsklick Einheit aus der Hand absetzen", TXT),
("Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)", TXT),
("Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)", TXT),
("Mausrad Zoomen", TXT),
("1-9, 0, X, V Werkzeug wählen", TXT),
("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT),
("G Kachel-Umrisse an/aus H Diese Hilfe", TXT),
("help.k_left", TXT),
("help.k_right", TXT),
("help.k_pan", TXT),
("help.k_rot", TXT),
("help.k_zoom", TXT),
("help.k_tools", TXT),
("help.k_misc", TXT),
("help.k_misc2", TXT),
("", TXT),
("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT),
("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT),
("3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.", TXT),
("4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.", TXT),
("5. Setze die Angriffsflagge nahe ans Tor und greife an!", TXT),
("help.s1", TXT),
("help.s2", TXT),
("help.s3", TXT),
("help.s4", TXT),
("help.s5", TXT),
("", TXT),
("Schrift: DINOBYTE von mby (CC BY 4.0)", DIM),
("help.font", DIM),
("", TXT),
("Klick zum Starten", FRAME_HI),
("help.start", FRAME_HI),
];
let lines: Vec<(String, Color)> = keys.iter().map(|(k, c)| (if k.is_empty() { String::new() } else { tr(k) }, *c)).collect();
let cols = 74;
let rows = lines.len() as i32 + 4;
let sw = screen_width();
@@ -449,6 +473,40 @@ impl Ui {
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
}
// First start: choose the language (names shown in their own language)
fn draw_langsel(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.6));
let langs = languages();
let cols = 50;
let rows = 6 + langs.len() as i32 * 3;
let x = ((sw - cols as f32 * CW) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH) * 0.5).max(0.0);
// the title shows all language names, since no language is chosen yet
let title = langs.iter().map(|(c, _)| crate::i18n::tr_in(c, "langsel.title")).collect::<Vec<_>>().join(" / ");
gfx.window_titled(x, y, cols, rows, S, true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
self.lang_rects.clear();
let (mx, my) = mouse_position();
for (k, (code, name)) in langs.iter().enumerate() {
let bw = 24.0 * CW;
let bx = (sw - bw) * 0.5;
let by = y + CH * (2.0 + k as f32 * 3.0);
let r = Rect::new(bx, by, bw, CH * 2.0);
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, if hov { FRAME_HI } else { FRAME });
gfx.text(name, bx + (bw - gfx.measure(name, S)) * 0.5, by + CH * 0.5, S, TXT);
self.lang_rects.push((r, code.clone()));
}
let hint_y = y + CH * (2.0 + langs.len() as f32 * 3.0 + 0.5);
for (k, (c, _)) in langs.iter().enumerate() {
let h = crate::i18n::tr_in(c, "langsel.hint");
gfx.text(&h, x + CW * 2.0, hint_y + CH * k as f32, 1.0, DIM);
}
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
}
fn draw_end(&mut self, g: &Game, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
@@ -460,13 +518,14 @@ impl Ui {
let y = (sh - rows as f32 * CH) * 0.5;
let col = if win { GOLD } else { RED };
gfx.window(x, y, cols, rows, S, true, col, Color::new(0.05, 0.03, 0.04, 0.97));
let title = if win { "SIEG!" } else { "NIEDERLAGE" };
gfx.text(title, (sw - gfx.measure(title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col);
let sub = if win { "Das Heldentor ist gefallen." } else { "Dein Seelenkern ist zerbrochen." };
gfx.text(sub, (sw - gfx.measure(sub, S)) * 0.5, y + CH * 4.5, S, TXT);
let st = format!("Spielzeit {} Wellen {} Armee {}", mmss(g.time), g.wave, g.army_value() as i32);
let title = tr(if win { "end.victory" } else { "end.defeat" });
gfx.text(&title, (sw - gfx.measure(&title, 4.0)) * 0.5, y + CH * 1.5, 4.0, col);
let sub = tr(if win { "end.victory_sub" } else { "end.defeat_sub" });
gfx.text(&sub, (sw - gfx.measure(&sub, S)) * 0.5, y + CH * 4.5, S, TXT);
let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]);
gfx.text(&st, (sw - gfx.measure(&st, S)) * 0.5, y + CH * 6.0, S, DIM);
let b = " Neues Spiel (N) ";
let b = format!(" {} ", tr("end.new"));
let b = b.as_str();
let bw = gfx.measure(b, S) + CW * 2.0;
let bx = (sw - bw) * 0.5;
let by = y + CH * 7.75;
@@ -480,80 +539,81 @@ impl Ui {
}
}
// Infotext für Einheit oder Kachel unter dem Mauszeiger (erste Zeile = Titel)
// Info text for the unit or tile under the mouse (first line = title)
pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
let mut out = Vec::new();
if let Some(j) = ent {
let e = &g.ents[j];
let s = st(e.kind);
out.push((format!("{} (Stufe {})", s.name, e.level), if e.hero { BLUE } else { FRAME_HI }));
out.push((format!("HP {}/{}", e.hp.max(0.0) as i32, e.max_hp as i32), GREEN));
out.push((job_name(e).to_string(), TXT));
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
out.push((tr(job_name(e)), TXT));
if e.hero {
out.push((if e.guard { "Torwächter".into() } else { "Eindringling".into() }, BLUE));
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
} else if e.creature() {
let pct = |v: f32| (v.clamp(0.0, 1.0) * 100.0).round() as i32;
out.push((format!("Energie {}% Hunger {}%", pct(e.energy), pct(e.hunger / 2.0)), TXT));
let pct = |v: f32| ((v.clamp(0.0, 1.0) * 100.0).round() as i32).to_string();
out.push((trf("info.needs", &[pct(e.energy), pct(e.hunger / 2.0)]), TXT));
let mood = if e.happy > 0.6 {
("zufrieden", GREEN)
("mood.happy", GREEN)
} else if e.happy > 0.25 {
("mürrisch", GOLD)
("mood.grumpy", GOLD)
} else {
("wütend", RED)
("mood.angry", RED)
};
out.push((format!("Laune: {}", mood.0), mood.1));
out.push((format!("Lohn: {} Gold", (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32), GOLD));
out.push((trf("info.mood", &[tr(mood.0)]), mood.1));
let wage = (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32;
out.push((trf("info.wage", &[wage.to_string()]), GOLD));
} else if e.carry > 0 {
out.push((format!("Trägt {} Gold", e.carry), GOLD));
out.push((trf("info.carries", &[e.carry.to_string()]), GOLD));
}
return out;
}
let Some((x, y)) = tile else { return out };
let Some(t) = g.map.get(x, y) else { return out };
let owner = match t.owner {
Owner::Player => "erobert",
Owner::Hero => "Heldengebiet",
Owner::None => "neutral",
Owner::Player => "owner.player",
Owner::Hero => "owner.hero",
Owner::None => "owner.none",
};
match t.kind {
Kind::Rock => out.push(("Fels".into(), DIM)),
Kind::Earth => out.push(("Erde".into(), FRAME_HI)),
Kind::Rock => out.push((tr("tile.rock"), DIM)),
Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)),
Kind::Gold => {
out.push(("Goldader".into(), GOLD));
out.push((format!("{} Gold", t.gold), GOLD));
out.push((tr("tile.gold"), GOLD));
out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD));
}
Kind::Floor => {
out.push(("Boden".into(), TXT));
out.push((owner.into(), DIM));
out.push((tr("tile.floor"), TXT));
out.push((tr(owner), DIM));
}
Kind::Core => {
out.push(("Seelenkern".into(), RED));
out.push((format!("HP {}/{}", g.core_hp as i32, CORE_HP as i32), RED));
out.push((tr("tile.core"), RED));
out.push((trf("info.hp", &[(g.core_hp as i32).to_string(), (CORE_HP as i32).to_string()]), RED));
}
Kind::Portal => {
out.push(("Portal".into(), MANA));
out.push((if g.portal_owned() { "erobert".into() } else { "neutral: Wichte können es erobern".into() }, DIM));
out.push((tr("tile.portal"), MANA));
out.push((tr(if g.portal_owned() { "portal.claimed" } else { "portal.neutral" }), DIM));
}
Kind::Gate => {
out.push(("Heldentor".into(), BLUE));
out.push((format!("HP {}/{}", g.gate_hp as i32, GATE_HP as i32), BLUE));
out.push((tr("tile.gate"), BLUE));
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));
}
k => {
if let Some(r) = room_index(k) {
out.push((ROOM_NAMES[r].into(), FRAME_HI));
out.push((tr(ROOM_NAMES[r]), FRAME_HI));
match k {
Kind::Farm => out.push((format!("{} Pilze", t.food as i32), GREEN)),
Kind::Lair => out.push((if t.bed != 0 { "Schlafplatz belegt".into() } else { "Schlafplatz frei".into() }, DIM)),
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
_ => {}
}
}
}
}
if t.kind == Kind::Rock {
out.push(("unzerstörbar".into(), DIM));
out.push((tr("tile.indestructible"), DIM));
}
if t.marked {
out.push(("zum Graben markiert".into(), GOLD));
out.push((tr("tile.marked"), GOLD));
}
out
}