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

+4 -1
View File
@@ -6,7 +6,10 @@ An original dungeon management game (genre: dungeon building & defence) that run
- Rust + **macroquad** (WebGL), target `wasm32-unknown-unknown`
- User interface drawn entirely **in the canvas**
- Font: **DINOBYTE** bitmap font (CP437, 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled
- All UI windows use **CP437 frames** (single lines for panels, double lines for dialogs); bars (HP/mana/gate) are pixel bars
- All UI windows use **CP437 frames** (single lines for panels, double lines for dialogs); **all bars are CP437 block characters** (█ ▌ ░)
- Text only in **fixed integer sizes**: 1×, 2× and 4× the 6×8 glyph size, times an integer **interface factor** (automatic from the device resolution, or 1×–4× in the settings)
- The interface is drawn in **device pixels** (high DPI) and laid out on the **character grid**, so every font pixel has exactly the same size
- **3D resolution** setting: the world can be rendered at 1/1, 1/2, 1/3 or 1/4 resolution and scaled up by exactly that factor (pixel look); the interface stays sharp
- Sound: event system + macroquad audio prepared (volume, mute key M); sounds will be generated later
- Languages: all in-game texts live in **language files** (`lang/*.lang`, key = text); English is the reference/fallback, German included, more can be added. The language is chosen on first start and stored in the browser.
- Repository language: **English** (code, comments, docs, commit messages)
+3
View File
@@ -333,3 +333,6 @@ dir.s = Süden
dir.sw = Südwesten
dir.w = Westen
dir.nw = Nordwesten
val.auto = Auto ({0}x)
set.render_scale = 3D-Auflösung
+3
View File
@@ -334,3 +334,6 @@ dir.s = south
dir.sw = south-west
dir.w = west
dir.nw = north-west
val.auto = Auto ({0}x)
set.render_scale = 3D resolution
+13 -10
View File
@@ -36,7 +36,7 @@ fn conf() -> macroquad::conf::Conf {
window_title: "DungeonCraft".into(),
window_width: 1280,
window_height: 800,
high_dpi: false,
high_dpi: true,
..Default::default()
},
draw_call_vertex_capacity: 40000,
@@ -67,10 +67,11 @@ fn next_seed(seed: u32) -> u32 {
fn apply_settings(s: &Settings, r3d: &mut R3d) {
i18n::set_language(&s.language);
ui::set_ui_scale(s.ui_scale as f32);
ui::set_ui_factor(s.ui_factor);
r3d.outlines = s.outlines;
r3d.set_wall_detail(s.wall_subdivisions());
r3d.max_lights = s.max_lights as usize;
r3d.render_div = s.render_scale.clamp(1, 4) as u32;
audio::set_volumes(s.music_volume, s.volume, s.muted);
}
@@ -238,7 +239,8 @@ async fn main() {
loop {
let dt = get_frame_time().min(0.1);
let now = get_time();
let (mx, my) = mouse_position();
let (mx, my) = mouse_position(); // CSS pixels (3D picking)
let (ux, uy) = ui::mouse(); // device pixels (interface)
let mut settings_changed = false;
// ---------------- savegame files: file picker result or dropped onto the window
@@ -272,7 +274,7 @@ async fn main() {
let dialog = ui.show_langsel || ui.show_help;
let in_menu = menu.open();
let playing = !in_menu && !dialog;
let over_ui = ui.over_ui(mx, my);
let over_ui = ui.over_ui(ux, uy);
let picked = app.cam.pick_tile(&app.game.map, mx, my);
let hover_tile = picked.map(|(x, z, _)| (x, z));
let ground = app.cam.ground_point(mx, my);
@@ -436,7 +438,7 @@ async fn main() {
// ---------------- left button
if is_mouse_button_pressed(MouseButton::Left) {
if dialog {
match ui.click(mx, my, &app.game) {
match ui.click(ux, uy, &app.game) {
UiAction::SelectLanguage(code) => {
app.settings.language = code;
settings_changed = true;
@@ -446,7 +448,7 @@ async fn main() {
_ => {}
}
} else if in_menu {
match menu.click(mx, my, &mut app.settings) {
match menu.click(ux, uy, &mut app.settings) {
MenuAction::SettingsChanged => settings_changed = true,
MenuAction::Resume => {}
MenuAction::NewGame => {
@@ -483,7 +485,7 @@ async fn main() {
MenuAction::None | MenuAction::Consumed => {}
}
} else {
match ui.click(mx, my, &app.game) {
match ui.click(ux, uy, &app.game) {
UiAction::SelectTool(t) => {
ui.tool = t;
app.game.sfx_ui(game::Sfx::Click);
@@ -520,8 +522,8 @@ async fn main() {
}
}
if inp.minimap_drag {
if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(mx, my) {
let (x, y) = ui.minimap_world(mx, my);
if is_mouse_button_down(MouseButton::Left) && ui.minimap_contains(ux, uy) {
let (x, y) = ui.minimap_world(ux, uy);
app.cam.tx = x;
app.cam.tz = y;
} else if !is_mouse_button_down(MouseButton::Left) {
@@ -591,8 +593,9 @@ async fn main() {
#[allow(clippy::too_many_arguments)]
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, inp: &Input, now: f64) {
let (mx, my) = mouse_position();
let (ux, uy) = ui::mouse();
let blocked = menu.open() || ui.show_help || ui.show_langsel;
let over_ui = blocked || ui.over_ui(mx, my);
let over_ui = blocked || ui.over_ui(ux, uy);
let hover_tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z));
let ground = app.cam.ground_point(mx, my);
clear_background(Color::from_rgba(4, 3, 5, 255));
+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));
}
+60 -25
View File
@@ -681,6 +681,8 @@ pub struct R3d {
pub outlines: bool, // tile outlines on/off
wall_sub: usize, // wall face subdivisions (detail)
pub max_lights: usize,
pub render_div: u32, // 3D resolution divisor (pixel look)
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
}
fn rgba(u: u32) -> Color {
@@ -710,7 +712,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
impl R3d {
pub fn new() -> R3d {
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, render_div: 1, target: None }
}
// Changing the wall detail rebuilds all chunks
@@ -854,7 +856,28 @@ impl R3d {
let m = cam.matrix();
self.update_chunks(g, gfx, &m);
EYE.with(|e| e.set(cam.eye_dir()));
set_camera(&cam.camera());
// pixel look: render the world at 1/div resolution and scale it up by exactly div
let div = self.render_div.max(1);
let d = crate::ui::dpr();
let (pw, ph) = ((screen_width() * d).round(), (screen_height() * d).round());
let mut cam3 = cam.camera();
if div > 1 {
let (tw, th) = ((pw / div as f32).ceil() as u32, (ph / div as f32).ceil() as u32);
if self.target.as_ref().map_or(true, |(_, w, h)| *w != tw || *h != th) {
let rt = render_target_ex(tw, th, RenderTargetParams { sample_count: 1, depth: true });
rt.texture.set_filter(FilterMode::Nearest);
self.target = Some((rt, tw, th));
}
let (rt, tw, th) = self.target.as_ref().unwrap();
cam3.render_target = Some(rt.clone());
cam3.aspect = Some(*tw as f32 / *th as f32);
} else {
self.target = None;
}
set_camera(&cam3);
if div > 1 {
clear_background(Color::from_rgba(4, 3, 5, 255));
}
// ---- lit geometry
let (lp, lc) = self.lights(g, cam, t);
@@ -919,9 +942,13 @@ impl R3d {
draw_fx(f, t);
}
gl_use_default_material();
set_default_camera();
crate::ui::begin_ui();
if let (true, Some((rt, tw, th))) = (div > 1, self.target.as_ref()) {
let size = vec2((*tw * div) as f32, (*th * div) as f32);
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(size), flip_y: true, ..Default::default() });
}
// ---- 2D overlay: health bars, sleep marks, levels, gate HP
// ---- 2D overlay in device pixels: health bars, sleep marks, levels, gate HP
self.draw_2d(g, gfx, cam, &m, t);
}
@@ -999,15 +1026,25 @@ impl R3d {
}
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {
let px_per_tile = screen_height() / cam.zoom;
use crate::ui::{ascii_bar, dpr, F, S, SMALL};
let d = dpr();
// projected positions are CSS pixels; the overlay is drawn in whole device pixels
let proj = |p: Vec3| {
let v = Cam::project(m, p) * d;
vec2(v.x.floor(), v.y.floor())
};
let small = SMALL();
let cw = GW * small; // width of one small block character
let px_per_tile = screen_height() / cam.zoom * d;
for e in &g.ents {
if !e.alive || e.held {
continue;
}
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
let head = Cam::project(m, vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
let w = (px_per_tile * 0.6).clamp(12.0, 40.0);
if e.hp < e.max_hp || e.hero {
let head = proj(vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
// bar width in block characters, depending on the zoom
let cols = ((px_per_tile * 0.6 / cw) as usize).clamp(3, 8);
if (e.hp < e.max_hp || e.hero) && e.captive == 0 {
let f = (e.hp / e.max_hp).clamp(0.0, 1.0);
let c = if e.hero {
Color::from_rgba(120, 180, 255, 255)
@@ -1018,40 +1055,38 @@ impl R3d {
} else {
Color::from_rgba(230, 60, 50, 255)
};
draw_rectangle(head.x - w * 0.5 - 1.0, head.y - 1.0, w + 2.0, 4.0, Color::new(0.0, 0.0, 0.0, 0.8));
draw_rectangle(head.x - w * 0.5, head.y, w * f, 2.0, c);
let x = head.x - (cols as f32 * cw * 0.5).floor();
ascii_bar(gfx, x, head.y - GH * small, cols, f, c, small);
}
if e.guard {
gfx.glyph(0x0F, head.x - 3.0, head.y - 11.0, 1.0, Color::from_rgba(255, 220, 90, 255));
gfx.glyph(0x0F, head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
}
if e.level > 1 && px_per_tile >= 24.0 {
gfx.text_sh(&e.level.to_string(), head.x - w * 0.5 - 8.0, head.y - 3.0, 1.0, Color::from_rgba(255, 220, 90, 255));
if e.level > 1 && px_per_tile >= 24.0 * F() as f32 {
let x = head.x - (cols as f32 * cw * 0.5).floor() - 2.0 * cw;
gfx.text_sh(&e.level.to_string(), x, head.y - GH * small, small, Color::from_rgba(255, 220, 90, 255));
}
if matches!(e.job, Job::Sleep(_)) && !e.moving {
let k = (t * 1.5 + e.anim) % 1.0;
gfx.glyph(b'z', head.x + w * 0.3, head.y - 6.0 - k * 14.0, 2.0, Color::new(0.8, 0.85, 1.0, 1.0 - k));
let rise = (k * 14.0 * F() as f32).floor();
gfx.glyph(b'z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
}
if e.carry > 0 {
let p = Cam::project(m, vec3(e.x, 0.35, e.y));
draw_circle(p.x + w * 0.35, p.y, 3.0, Color::from_rgba(250, 210, 60, 255));
let p = proj(vec3(e.x, 0.35, e.y));
gfx.glyph(0x0F, p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
}
}
for tw in &g.towers {
if tw.hp < tw.max_hp() {
let (x, z) = tw.pos();
let p = Cam::project(m, vec3(x, tower_height(tw) + 0.9, z));
let w = 40.0;
let f = (tw.hp / tw.max_hp()).clamp(0.0, 1.0);
draw_rectangle(p.x - w * 0.5 - 1.0, p.y - 1.0, w + 2.0, 5.0, BLACK);
draw_rectangle(p.x - w * 0.5, p.y, w * f, 3.0, Color::from_rgba(255, 160, 60, 255));
let p = proj(vec3(x, tower_height(tw) + 0.9, z));
ascii_bar(gfx, p.x - (3.0 * cw).floor(), p.y, 6, tw.hp / tw.max_hp(), Color::from_rgba(255, 160, 60, 255), small);
}
}
if g.gate_hp > 0.0 && g.gate_hp < GATE_HP {
let (gx, gz) = g.gate_center();
let p = Cam::project(m, vec3(gx, 2.4, gz));
let w = 100.0;
draw_rectangle(p.x - w * 0.5 - 1.0, p.y - 1.0, w + 2.0, 8.0, BLACK);
draw_rectangle(p.x - w * 0.5, p.y, w * g.gate_hp / GATE_HP, 6.0, Color::from_rgba(120, 180, 255, 255));
let p = proj(vec3(gx, 2.4, gz));
let cols = 12;
ascii_bar(gfx, p.x - (cols as f32 * GW * S() * 0.5).floor(), p.y, cols, g.gate_hp / GATE_HP, Color::from_rgba(120, 180, 255, 255), S());
}
}
}
+4 -2
View File
@@ -141,7 +141,8 @@ pub struct Settings {
pub outlines: bool,
pub wall_detail: u8, // 1 low, 2 medium, 3 high
pub max_lights: u8, // 8, 16 or 24
pub ui_scale: u8, // font scale 2 (normal) or 3 (large)
pub ui_factor: u8, // interface size: 0 = automatic, 1..4 = fixed integer factor
pub render_scale: u8, // 3D world resolution divisor: 1 = full, 2..4 = pixel look
// game
pub game_speed: f32, // 1 or 2
pub hints: bool,
@@ -165,7 +166,8 @@ impl Default for Settings {
outlines: true,
wall_detail: 2,
max_lights: 24,
ui_scale: 2,
ui_factor: 0,
render_scale: 1,
game_speed: 1.0,
hints: true,
map_w: 64,
+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);