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

+58 -48
View File
@@ -5,7 +5,7 @@ use crate::gfx::*;
use crate::i18n::{languages, tr, trf};
use crate::map::{MAX_SIZE, MIN_SIZE};
use crate::settings::{all_actions, key_name, Action, Settings};
use crate::ui::{tool_info, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, TOOLS, TXT};
use crate::ui::{grid_center, grid_center_y, mouse, sh, sw, tool_info, CH, CW, DIM, FRAME, FRAME_HI, GOLD, S, SMALL, TITLE, TOOLS, TXT};
use macroquad::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -34,6 +34,7 @@ pub enum Item {
WallDetail,
Lights,
UiScale,
RenderScale,
GameSpeed,
Hints,
}
@@ -43,7 +44,7 @@ fn tab_items(tab: usize) -> &'static [Item] {
match tab {
0 => &[Language, Volume, MusicVolume, Mute],
1 => &[RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
2 => &[Outlines, WallDetail, Lights, UiScale],
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
3 => &[GameSpeed, Hints],
_ => &[],
}
@@ -63,6 +64,7 @@ fn item_label(it: Item) -> String {
Item::WallDetail => "set.wall_detail",
Item::Lights => "set.lights",
Item::UiScale => "set.ui_scale",
Item::RenderScale => "set.render_scale",
Item::GameSpeed => "set.game_speed",
Item::Hints => "set.hints",
})
@@ -93,7 +95,14 @@ fn item_value(it: Item, s: &Settings) -> String {
_ => "val.medium",
}),
Item::Lights => s.max_lights.to_string(),
Item::UiScale => tr(if s.ui_scale >= 3 { "val.large" } else { "val.normal" }),
Item::UiScale => {
if s.ui_factor == 0 {
trf("val.auto", &[crate::ui::F().to_string()])
} else {
format!("{}x", s.ui_factor)
}
}
Item::RenderScale => format!("1/{}", s.render_scale.max(1)),
Item::GameSpeed => format!("{}x", s.game_speed as i32),
Item::Hints => onoff(s.hints),
}
@@ -126,7 +135,8 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
Item::Outlines => s.outlines = !s.outlines,
Item::WallDetail => s.wall_detail = cycle(&[1u8, 2, 3], s.wall_detail, d),
Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d),
Item::UiScale => s.ui_scale = cycle(&[2u8, 3], s.ui_scale, d),
Item::UiScale => s.ui_factor = cycle(&[0u8, 1, 2, 3, 4], s.ui_factor, d),
Item::RenderScale => s.render_scale = cycle(&[1u8, 2, 3, 4], s.render_scale.max(1), d),
Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d),
Item::Hints => s.hints = !s.hints,
}
@@ -285,8 +295,8 @@ impl Menu {
pub fn draw(&mut self, gfx: &Gfx, settings: &Settings, status: Option<&str>) {
self.buttons.clear();
let Some(page) = self.page else { return };
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, if page == Page::Title { 0.55 } else { 0.45 }));
match page {
Page::Title => self.draw_title(gfx),
@@ -299,8 +309,8 @@ impl Menu {
let a = all_actions()[action];
let text = trf("set.press_key", &[action_label(a)]);
let cols = (text.chars().count() as i32 + 8).max(36);
let x = (sw - cols as f32 * CW()) * 0.5;
let y = sh * 0.4;
let x = grid_center(sw, cols as f32 * CW());
let y = (sh * 0.4 / CH()).floor() * CH();
gfx.window(x, y, cols, 5, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.98));
gfx.text(&text, x + CW() * 4.0, y + CH() * 1.5, S(), TXT);
let esc = tr("set.esc_cancel");
@@ -309,7 +319,7 @@ impl Menu {
}
if let Some(st) = status {
let w = gfx.measure(st, S()) + CW() * 4.0;
let x = (sw - w) * 0.5;
let x = grid_center(sw, w);
let y = sh - CH() * 9.0;
draw_rectangle(x, y, w, CH() * 2.0, Color::new(0.05, 0.03, 0.04, 0.95));
draw_rectangle_lines(x, y, w, CH() * 2.0, 2.0, GOLD);
@@ -318,24 +328,24 @@ impl Menu {
}
fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) {
let (mx, my) = mouse_position();
let (mx, my) = mouse();
for (k, (b, label)) in items.iter().enumerate() {
let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.2);
let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.0);
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
self.buttons.push((r, *b));
}
}
fn draw_title(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
let big = (S() * 4.0).min((sw / (GW * 14.0)).floor()).max(S());
let sw = sw();
let sh = sh();
let big = TITLE();
let title = "DungeonCraft";
let ty = sh * 0.12;
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5 + big, ty + big, big, Color::new(0.0, 0.0, 0.0, 0.8));
gfx.text(title, (sw - gfx.measure(title, big)) * 0.5, ty, big, FRAME_HI);
let ty = (sh * 0.12 / CH()).floor() * CH();
gfx.text(title, grid_center(sw, gfx.measure(title, big)) + big, ty + big, big, Color::new(0.0, 0.0, 0.0, 0.8));
gfx.text(title, grid_center(sw, gfx.measure(title, big)), ty, big, FRAME_HI);
let sub = tr("menu.subtitle");
gfx.text(&sub, (sw - gfx.measure(&sub, S())) * 0.5, ty + GH * big + CH(), S(), TXT);
gfx.text(&sub, grid_center(sw, gfx.measure(&sub, S())), ty + GH * big + CH(), S(), TXT);
let mut items = Vec::new();
if self.has_autosave {
items.push((Btn::Continue, tr("menu.continue")));
@@ -346,35 +356,35 @@ impl Menu {
items.push((Btn::Help, tr("menu.help")));
let w = 26.0 * CW();
let y = ty + GH * big + CH() * 4.0;
self.column(gfx, (sw - w) * 0.5, y, w, &items);
self.column(gfx, grid_center(sw, w), y, w, &items);
let hint = tr("menu.drop_hint");
gfx.text(&hint, (sw - gfx.measure(&hint, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 7.5, (S() / 2.0).max(1.0), DIM);
gfx.text(&hint, grid_center(sw, gfx.measure(&hint, SMALL())), sh - CH() * 8.0, SMALL(), DIM);
let credit = tr("help.font");
gfx.text(&credit, (sw - gfx.measure(&credit, (S() / 2.0).max(1.0))) * 0.5, sh - CH() * 6.5, (S() / 2.0).max(1.0), DIM);
gfx.text(&credit, grid_center(sw, gfx.measure(&credit, SMALL())), sh - CH() * 7.0, SMALL(), DIM);
}
// New game: choose the map size (width and height separately)
fn draw_new_game(&mut self, gfx: &Gfx, s: &Settings) {
let sw = screen_width();
let sh = screen_height();
let (mx, my) = mouse_position();
let sw = sw();
let sh = sh();
let (mx, my) = mouse();
let cols = 44;
let rows = 17;
let x = (sw - cols as f32 * CW()) * 0.5;
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
let x = grid_center(sw, cols as f32 * CW());
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.new"), FRAME_HI);
let lines = [("newgame.width", s.map_w, 0), ("newgame.height", s.map_h, 1)];
for (k, &(key, val, which)) in lines.iter().enumerate() {
let by = y + CH() * (2.0 + k as f32 * 2.5);
gfx.text(&tr(key), x + CW() * 3.0, by + CH() * 0.4, S(), TXT);
gfx.text(&tr(key), x + CW() * 3.0, by + CH() * 0.5, S(), TXT);
let vw = 14.0 * CW();
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
let left = Rect::new(vx, by, CW() * 2.5, CH() * 1.8);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 1.8);
let left = Rect::new(vx, by, CW() * 2.5, CH() * 2.0);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 2.0);
button(gfx, left, "<", left.contains(vec2(mx, my)), FRAME);
button(gfx, right, ">", right.contains(vec2(mx, my)), FRAME);
let v = val.to_string();
gfx.text(&v, vx + (vw - gfx.measure(&v, S())) * 0.5, by + CH() * 0.4, S(), GOLD);
gfx.text(&v, vx + (vw - gfx.measure(&v, S())) * 0.5, by + CH() * 0.5, S(), GOLD);
let (l, r) = if which == 0 { (Btn::MapW(-1), Btn::MapW(1)) } else { (Btn::MapH(-1), Btn::MapH(1)) };
self.buttons.push((left, l));
self.buttons.push((right, r));
@@ -389,8 +399,8 @@ impl Menu {
}
fn draw_pause(&mut self, gfx: &Gfx) {
let sw = screen_width();
let sh = screen_height();
let sw = sw();
let sh = sh();
let items = vec![
(Btn::Resume, tr("menu.resume")),
(Btn::Save, tr("menu.save")),
@@ -401,21 +411,21 @@ impl Menu {
];
let cols = 32;
let rows = items.len() as i32 * 3 + 4;
let x = (sw - cols as f32 * CW()) * 0.5;
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
let x = grid_center(sw, cols as f32 * CW());
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.paused"), FRAME_HI);
let w = (cols - 6) as f32 * CW();
self.column(gfx, x + CW() * 3.0, y + CH() * 2.0, w, &items);
}
fn draw_settings(&mut self, gfx: &Gfx, s: &Settings, tab: usize) {
let sw = screen_width();
let sh = screen_height();
let (mx, my) = mouse_position();
let sw = sw();
let sh = sh();
let (mx, my) = mouse();
let cols = ((sw / CW()) as i32 - 2).min(66);
let rows = ((sh / CH()) as i32 - 2).min(34);
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);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.settings"), FRAME_HI);
// tabs
let mut tx = x + CW() * 2.0;
@@ -437,7 +447,7 @@ impl Menu {
let acts = all_actions();
let half = (acts.len() + 1) / 2;
let colw = (cols as f32 - 6.0) * CW() * 0.5;
let rowh = CH() * 1.35;
let rowh = CH();
for (k, a) in acts.iter().enumerate() {
let c = k / half;
let r = k % half;
@@ -456,32 +466,32 @@ impl Menu {
self.buttons.push((rect, Btn::Key(k)));
}
let hint = tr("set.keys_hint");
gfx.text(&hint, x + CW() * 2.0, top + half as f32 * rowh + CH() * 0.5, (S() / 2.0).max(1.0), DIM);
gfx.text(&hint, x + CW() * 2.0, top + half as f32 * rowh + CH() * 0.5, SMALL(), DIM);
let rw = gfx.measure(&tr("set.reset_keys"), S()) + CW() * 4.0;
let rr = Rect::new(x + cols as f32 * CW() - rw - CW() * 2.0, y + (rows - 4) as f32 * CH(), rw, CH() * 2.2);
let rr = Rect::new(x + cols as f32 * CW() - rw - CW() * 2.0, y + (rows - 4) as f32 * CH(), rw, CH() * 2.0);
button(gfx, rr, &tr("set.reset_keys"), rr.contains(vec2(mx, my)), FRAME);
self.buttons.push((rr, Btn::ResetKeys));
} else {
for (i, &it) in tab_items(tab).iter().enumerate() {
let by = top + i as f32 * CH() * 2.5;
gfx.text(&item_label(it), x + CW() * 3.0, by + CH() * 0.4, S(), TXT);
gfx.text(&item_label(it), x + CW() * 3.0, by + CH() * 0.5, S(), TXT);
// value with < > buttons
let vw = 18.0 * CW();
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
let left = Rect::new(vx, by, CW() * 2.5, CH() * 1.8);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 1.8);
let left = Rect::new(vx, by, CW() * 2.5, CH() * 2.0);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 2.0);
button(gfx, left, "<", left.contains(vec2(mx, my)), FRAME);
button(gfx, right, ">", right.contains(vec2(mx, my)), FRAME);
let val = item_value(it, s);
gfx.text(&val, vx + (vw - gfx.measure(&val, S())) * 0.5, by + CH() * 0.4, S(), GOLD);
let mid = Rect::new(vx + CW() * 2.5, by, vw - CW() * 5.0, CH() * 1.8);
gfx.text(&val, vx + (vw - gfx.measure(&val, S())) * 0.5, by + CH() * 0.5, S(), GOLD);
let mid = Rect::new(vx + CW() * 2.5, by, vw - CW() * 5.0, CH() * 2.0);
self.buttons.push((left, Btn::Change(i, -1)));
self.buttons.push((right, Btn::Change(i, 1)));
self.buttons.push((mid, Btn::Change(i, 1)));
}
}
let bw = gfx.measure(&tr("menu.back"), S()) + CW() * 6.0;
let br = Rect::new(x + CW() * 2.0, y + (rows - 4) as f32 * CH(), bw, CH() * 2.2);
let br = Rect::new(x + CW() * 2.0, y + (rows - 4) as f32 * CH(), bw, CH() * 2.0);
button(gfx, br, &tr("menu.back"), br.contains(vec2(mx, my)), FRAME);
self.buttons.push((br, Btn::Back));
}