UI in device pixels on the character grid with fixed integer font sizes, ASCII bars; 3D render scale for a pixel look

- text only at 1x/2x/4x times an integer interface factor (auto or 1-4 in the settings)
- high DPI rendering, own 2D camera in device pixels, mouse converted
- toolbar buttons 6 columns, integer icon scale, minimap integer pixels per tile
- all bars (top bar, unit/tower/gate HP) as CP437 block characters
- 3D world optionally rendered at 1/2, 1/3, 1/4 and upscaled with nearest filtering

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 08:42:50 +01:00
1 parent 889ca66dbf
commit 50d013bb28
8 files changed
+344 -213

No files matched your search

+199 -127
View File
@@ -9,18 +9,58 @@ use crate::render3d::{minimap_color, Cam};
use crate::settings::{key_name, Action, Settings};
use macroquad::prelude::*;
// UI font scale (setting: 2 = normal, 3 = large); character cell size follows it
// ---- interface scaling
// The interface is drawn in device pixels (see begin_ui). Every size is an integer multiple
// of the font's 6x8 pixel glyphs: small text = 1*F, normal text = 2*F, titles = 4*F, where F
// is the interface factor (automatic from the resolution or fixed in the settings).
thread_local! {
static UI_SCALE: std::cell::Cell<f32> = std::cell::Cell::new(2.0);
static UI_FACTOR_SETTING: std::cell::Cell<u8> = std::cell::Cell::new(0);
}
pub fn set_ui_scale(s: f32) {
UI_SCALE.with(|c| c.set(s));
pub fn set_ui_factor(f: u8) {
UI_FACTOR_SETTING.with(|c| c.set(f));
}
// device pixels per CSS pixel
pub fn dpr() -> f32 {
screen_dpi_scale()
}
// screen size and mouse in device pixels
pub fn sw() -> f32 {
(screen_width() * dpr()).round()
}
pub fn sh() -> f32 {
(screen_height() * dpr()).round()
}
pub fn mouse() -> (f32, f32) {
let (x, y) = mouse_position();
(x * dpr(), y * dpr())
}
// interface factor (integer)
#[allow(non_snake_case)]
pub fn F() -> u32 {
let set = UI_FACTOR_SETTING.with(|c| c.get());
if set > 0 {
return set as u32;
}
((sw() / 1280.0).min(sh() / 760.0).floor() as u32).clamp(1, 4)
}
// text scales: 1x, 2x and 4x the glyph size, times F
#[allow(non_snake_case)]
pub fn SMALL() -> f32 {
F() as f32
}
#[allow(non_snake_case)]
pub fn S() -> f32 {
UI_SCALE.with(|c| c.get())
2.0 * F() as f32
}
#[allow(non_snake_case)]
pub fn TITLE() -> f32 {
4.0 * F() as f32
}
// character cell of normal text = the layout grid
#[allow(non_snake_case)]
pub fn CW() -> f32 {
GW * S()
}
@@ -28,10 +68,44 @@ pub fn CW() -> f32 {
pub fn CH() -> f32 {
GH * S()
}
// factor relative to the normal scale, for pixel sizes such as buttons and icons
// pixel factor for icons and other pixel art
#[allow(non_snake_case)]
pub fn K() -> f32 {
S() / 2.0
F() as f32
}
// x position that centres a width on the character grid
pub fn grid_center(total: f32, w: f32) -> f32 {
(((total - w) * 0.5) / CW()).floor() * CW()
}
pub fn grid_center_y(total: f32, h: f32) -> f32 {
(((total - h) * 0.5) / CH()).floor() * CH()
}
// 2D camera mapping one unit to one device pixel (top-left origin)
pub fn begin_ui() {
let (w, h) = (sw(), sh());
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
}
// bar made of CP437 block characters: full blocks, a half block, shaded rest
pub fn ascii_bar(gfx: &Gfx, x: f32, y: f32, cols: usize, frac: f32, col: Color, scale: f32) {
let f = frac.clamp(0.0, 1.0) * cols as f32;
let full = f.floor() as usize;
let half = f - full as f32 >= 0.5;
let dim = Color::new(col.r * 0.35, col.g * 0.35, col.b * 0.35, col.a);
for k in 0..cols {
let px = x + k as f32 * GW * scale;
if k < full {
gfx.glyph(0xDB, px, y, scale, col);
} else if k == full && half {
gfx.glyph(0xB0, px, y, scale, dim);
gfx.glyph(0xDD, px, y, scale, col);
} else {
gfx.glyph(0xB0, px, y, scale, dim);
}
}
}
pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0);
@@ -176,12 +250,6 @@ fn wrap(s: &str, cols: usize) -> Vec<String> {
out
}
fn bar(x: f32, y: f32, w: f32, h: f32, frac: f32, col: Color) {
draw_rectangle(x - 1.0, y - 1.0, w + 2.0, h + 2.0, Color::new(0.0, 0.0, 0.0, 1.0));
draw_rectangle(x, y, w, h, Color::new(col.r * 0.25, col.g * 0.25, col.b * 0.25, 1.0));
draw_rectangle(x, y, w * frac.clamp(0.0, 1.0), h, col);
draw_rectangle(x, y, w * frac.clamp(0.0, 1.0), (h * 0.3).max(1.0), Color::new(1.0, 1.0, 1.0, 0.25));
}
fn mmss(t: f32) -> String {
let t = t.max(0.0) as i32;
@@ -259,64 +327,86 @@ impl Ui {
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, settings: &Settings, info: &[(String, Color)], now: f64) {
self.rects.clear();
let keys: Vec<String> = (0..TOOLS.len()).map(|i| key_name(settings.key(Action::Tool(i)))).collect();
let sw = screen_width();
let sh = screen_height();
let (mx, my) = mouse_position();
let sw = sw();
let sh = sh();
let (mx, my) = mouse();
// ---------------- top bar
let cols = (sw / CW()).ceil() as i32;
// ---------------- top bar (everything on the character grid)
let cols = (sw / CW()).floor() 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()));
let y = CH();
let mut x = CW() * 2.0;
let seg = |label: &str, lc: Color, value: &str, vc: Color, x: &mut f32| {
gfx.text(label, *x, y, S(), lc);
*x += gfx.measure(label, S()) + CW();
gfx.text(value, *x, y, S(), vc);
*x += gfx.measure(value, S()) + CW() * 2.0;
let mut c = 2usize; // current column
let col_x = |c: usize| c as f32 * CW();
let put = |s: &str, color: Color, c: &mut usize| {
gfx.text(s, col_x(*c), y, S(), color);
*c += s.chars().count();
};
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;
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;
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 bar = |frac: f32, color: Color, c: &mut usize| {
ascii_bar(gfx, col_x(*c), y, 6, frac, color, S());
*c += 6;
};
put(&tr("hud.gold"), GOLD, &mut c);
c += 1;
put(&format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT, &mut c);
c += 2;
put(&tr("hud.mana"), MANA, &mut c);
c += 1;
bar(g.mana / MANA_MAX, MANA, &mut c);
c += 1;
put(&format!("{:<3}", g.mana as i32), TXT, &mut c);
c += 2;
put(&tr("hud.core"), RED, &mut c);
c += 1;
bar(g.core_hp / CORE_HP, RED, &mut c);
c += 2;
put(&tr("hud.gate"), BLUE, &mut c);
c += 1;
bar(g.gate_hp / GATE_HP, BLUE, &mut c);
c += 2;
let cap = g.counts[1].min(30);
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 mut segs: Vec<(String, Color, String)> = vec![
(tr("hud.creatures"), TXT, format!("{}/{}", g.creature_count(), cap)),
(tr("hud.workers"), TXT, g.worker_count().to_string()),
];
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
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);
segs.push((trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col, mmss(g.wave_timer)));
segs.push((tr("hud.army"), TXT, (g.army_value() as i32).to_string()));
if !g.hand.is_empty() {
segs.push((tr("hud.hand"), FRAME_HI, format!("{}/8", g.hand.len())));
}
let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
// volume only if it still fits into the bar
if x + gfx.measure(&format!("♪ {}", vol), S()) < sw - CW() * 2.0 {
seg("♪", DIM, &vol, DIM, &mut x);
segs.push(("\u{266a}".to_string(), DIM, vol));
for (label, lc, value) in segs {
// only what still fits into the bar
if c + label.chars().count() + value.chars().count() + 2 > cols as usize - 1 {
break;
}
put(&label, lc, &mut c);
c += 1;
put(&value, if lc == DIM { DIM } else { TXT }, &mut c);
c += 2;
}
// ---------------- toolbar
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
let tb_rows = 5;
let tb_y = sh - tb_rows as f32 * CH();
let tb_y = (tb_y / CH()).floor() * CH();
let tb_h = sh - tb_y;
let tb_rows = (tb_h / CH()).floor() as i32;
gfx.window(0.0, tb_y, cols, tb_rows, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, tb_y, sw, tb_rows as f32 * CH()));
self.rects.push(Rect::new(0.0, tb_y, sw, tb_h));
self.tool_rects.clear();
let bw = (80.0 * K()).min(((sw - CW() * 2.0 - 150.0) / TOOLS.len() as f32 - 4.0 * K()).max(48.0));
let mut hover_tool = None;
let bw = 6.0 * CW();
let px = K(); // one pixel of the font, times the factor
for (i, &t) in TOOLS.iter().enumerate() {
let bx = CW() + i as f32 * (bw + 4.0 * K());
let by = tb_y + CH() * 0.75;
let r = Rect::new(bx, by, bw, CH() * 3.5);
let bx = CW() + i as f32 * bw;
let by = tb_y + CH();
if bx + bw > sw - CW() {
break;
}
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
self.tool_rects.push(r);
let hovered = r.contains(vec2(mx, my));
if hovered {
@@ -324,43 +414,27 @@ impl Ui {
}
let sel = self.tool == t;
draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.35, 0.22, 0.1, 1.0) } else 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, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
let ti = tool_info(t);
let icon = 32.0 * K();
let small = (S() / 2.0).floor().max(1.0);
gfx.draw_cell(ti.icon, bx + (bw - icon) * 0.5, by + 4.0 * K(), icon, icon, WHITE);
gfx.text(&keys[i], bx + 3.0, by + 3.0, small, if sel { FRAME_HI } else { DIM });
let lw = gfx.measure(&ti.label, small);
gfx.text(&ti.label, bx + (bw - lw) * 0.5, by + 42.0 * K(), small, if sel { FRAME_HI } else { TXT });
}
// hand contents
let hx = CW() + TOOLS.len() as f32 * (bw + 4.0 * K()) + CW();
if hx + 120.0 < sw - 200.0 {
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;
let sy = tb_y + CH() * 2.2 + (k / 4) as f32 * 24.0;
draw_texture_ex(
&gfx.sprites,
sx,
sy,
WHITE,
DrawTextureParams { dest_size: Some(vec2(20.0, 20.0)), source: Some(Gfx::sprite_rect(e.kind, false)), ..Default::default() },
);
}
}
if g.hand.is_empty() {
gfx.text(&tr("hud.empty"), hx, tb_y + CH() * 2.5, 1.0, DIM);
}
// icon: the 16 px atlas cell at 2*F, centred on the grid
let icon = 32.0 * px;
gfx.draw_cell(ti.icon, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, WHITE);
gfx.text(&keys[i], bx + 3.0 * px, by + 3.0 * px, SMALL(), if sel { FRAME_HI } else { DIM });
// label in small text, centred on whole small-character cells
let label: String = ti.label.chars().take(11).collect();
let lw = gfx.measure(&label, SMALL());
let lx = bx + ((bw - lw) / (GW * SMALL()) / 2.0).floor() * GW * SMALL();
gfx.text(&label, lx, by + 2.0 * CH() + 4.0 * px, SMALL(), if sel { FRAME_HI } else { TXT });
}
// ---------------- minimap
let mm_cols = 16;
let mm_rows = 12;
// ---------------- minimap: whole device pixels per tile
let ms = ((14.0 * CW()) / mw().max(mh()) as f32).floor().max(1.0);
let (map_w, map_h) = (mw() as f32 * ms, mh() as f32 * ms);
let mm_cols = ((map_w / CW()).ceil() as i32 + 2).max(10);
let mm_rows = (map_h / CH()).ceil() as i32 + 2;
let mm_w = mm_cols as f32 * CW();
let mm_h = mm_rows as f32 * CH();
let mm_x = sw - mm_w;
let mm_x = ((sw - mm_w) / CW()).floor() * CW();
let mm_y = tb_y - mm_h;
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));
@@ -383,39 +457,37 @@ impl Ui {
}
self.minimap_tex.update(&self.minimap_img);
}
// keep the map's aspect ratio inside the window
let avail = (mm_w - CW() * 2.0).min(mm_h - CH() * 2.0);
let ms = avail / mw().max(mh()) as f32;
let (map_w, map_h) = (mw() as f32 * ms, mh() as f32 * ms);
let mrx = mm_x + (mm_w - map_w) * 0.5;
let mry = mm_y + CH() + (mm_h - CH() * 2.0 - map_h) * 0.5;
let mrx = (mm_x + (mm_w - map_w) * 0.5).floor();
let mry = (mm_y + (mm_h - map_h) * 0.5).floor();
self.minimap_rect = Rect::new(mrx, mry, map_w, map_h);
draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_w, map_h)), ..Default::default() });
let dot = (2.0 * px).max(ms);
for e in &g.ents {
if e.alive && !e.held {
let c = if e.hero { Color::new(0.6, 0.85, 1.0, 1.0) } else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) };
draw_rectangle(mrx + e.x * ms - 1.0, mry + e.y * ms - 1.0, 3.0, 3.0, c);
draw_rectangle((mrx + e.x.floor() * ms).floor(), (mry + e.y.floor() * ms).floor(), dot, dot, c);
}
}
if let Some(f) = g.flag {
let (fx, fy) = xy(f);
draw_rectangle(mrx + fx as f32 * ms - 2.0, mry + fy as f32 * ms - 2.0, 5.0, 5.0, RED);
draw_rectangle(mrx + fx as f32 * ms - px, mry + fy as f32 * ms - px, dot + 2.0 * px, dot + 2.0 * px, RED);
}
// expected entry points of the hero wave (blinking)
if (now * 3.0) as i64 % 2 == 0 {
for &(ex, ey) in &g.planned_entries {
let (px, py) = (mrx + ex as f32 * ms, mry + ey as f32 * ms);
draw_rectangle_lines(px - 4.0, py - 4.0, 9.0, 9.0, 2.0, Color::new(0.6, 0.85, 1.0, 1.0));
let (ppx, ppy) = (mrx + ex as f32 * ms, mry + ey as f32 * ms);
draw_rectangle_lines(ppx - 4.0 * px, ppy - 4.0 * px, 9.0 * px, 9.0 * px, 2.0 * px, Color::new(0.6, 0.85, 1.0, 1.0));
}
}
// camera footprint on the ground
let corners = [cam.ground_point(0.0, 0.0), cam.ground_point(sw, 0.0), cam.ground_point(sw, sh), cam.ground_point(0.0, sh)];
// camera footprint on the ground (the 3D camera works in CSS pixels)
let (lw, lh) = (screen_width(), screen_height());
let corners = [cam.ground_point(0.0, 0.0), cam.ground_point(lw, 0.0), cam.ground_point(lw, lh), cam.ground_point(0.0, lh)];
for k in 0..4 {
let a = corners[k];
let b = corners[(k + 1) % 4];
let clampp = |p: Vec2| vec2(mrx + p.x.clamp(0.0, mw() as f32) * ms, mry + p.y.clamp(0.0, mh() as f32) * ms);
let (pa, pb) = (clampp(a), clampp(b));
draw_line(pa.x, pa.y, pb.x, pb.y, 1.5, WHITE);
draw_line(pa.x, pa.y, pb.x, pb.y, px, WHITE);
}
// ---------------- info panel
@@ -471,8 +543,8 @@ impl Ui {
}
lines.push((trf("ui.key", &[keys[i].clone()]), 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;
let tx = (((mx - tcols as f32 * CW() * 0.5) / CW()).floor() * CW()).clamp(0.0, sw - tcols as f32 * CW());
let ty = tb_y - rows as f32 * CH();
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);
@@ -483,8 +555,8 @@ impl Ui {
// Dialogs on top of everything (help, language selection, end screen, pause)
pub fn draw_dialogs(&mut self, g: &Game, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
let sw = sw();
let sh = sh();
if self.show_langsel {
self.draw_langsel(gfx);
} else if g.state != 0 {
@@ -493,13 +565,13 @@ impl Ui {
self.draw_help(gfx);
} else if g.paused {
let w = 24;
let x = (sw - w as f32 * CW()) * 0.5;
let x = grid_center(sw, w as f32 * CW());
let y = sh * 0.35;
gfx.window(x, y, w, 5, S(), true, FRAME_HI, BG);
let t = tr("ui.pause");
gfx.text(&t, (sw - gfx.measure(&t, 3.0)) * 0.5, y + CH(), 3.0, FRAME_HI);
gfx.text(&t, grid_center(sw, gfx.measure(&t, TITLE())), y + CH(), TITLE(), FRAME_HI);
let t2 = tr("ui.pause_hint");
gfx.text(&t2, (sw - gfx.measure(&t2, S())) * 0.5, y + CH() * 3.0, S(), DIM);
gfx.text(&t2, grid_center(sw, gfx.measure(&t2, S())), y + CH() * 3.0, S(), DIM);
}
}
@@ -529,10 +601,10 @@ impl Ui {
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();
let sh = screen_height();
let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
let sw = sw();
let sh = sh();
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), "DungeonCraft", FRAME_HI);
for (k, (l, c)) in lines.iter().enumerate() {
gfx.text(l, x + CW() * 3.0, y + CH() * (k as f32 + 2.0), S(), *c);
@@ -542,22 +614,22 @@ impl Ui {
// 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();
let sw = sw();
let sh = sh();
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);
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);
// 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();
let (mx, my) = mouse();
for (k, (code, name)) in langs.iter().enumerate() {
let bw = 24.0 * CW();
let bx = (sw - bw) * 0.5;
let bx = grid_center(sw, bw);
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));
@@ -569,35 +641,35 @@ impl Ui {
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);
gfx.text(&h, x + CW() * 2.0, hint_y + CH() * k as f32, SMALL(), 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();
let sw = sw();
let sh = sh();
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.5));
let win = g.state == 1;
let cols = 44;
let rows = 11;
let x = (sw - cols as f32 * CW()) * 0.5;
let y = (sh - rows as f32 * CH()) * 0.5;
let x = grid_center(sw, cols as f32 * CW());
let y = grid_center_y(sh, rows as f32 * CH());
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 = 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);
gfx.text(&title, grid_center(sw, gfx.measure(&title, TITLE())), y + CH(), TITLE(), 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);
gfx.text(&sub, grid_center(sw, gfx.measure(&sub, S())), 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);
gfx.text(&st, grid_center(sw, gfx.measure(&st, S())), y + CH() * 6.0, S(), DIM);
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 bx = grid_center(sw, bw);
let by = y + CH() * 7.75;
self.end_button = Rect::new(bx, by, bw, CH() * 2.0);
let (mx, my) = mouse_position();
let (mx, my) = mouse();
let hov = self.end_button.contains(vec2(mx, my));
draw_rectangle(bx, by, bw, CH() * 2.0, 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(bx, by, bw, CH() * 2.0, 2.0, col);