Title screen, pause menu, settings dialog and savegames

- title screen (continue/new/load/settings/help) and Esc pause menu
- settings: language & sound, camera, graphics, game, rebindable keys;
  stored in localStorage and in every savegame (always applied on load)
- savegames as JSON (.dcsave): download, load via file picker or drag & drop
- autosave to localStorage every 2 minutes and when the tab is hidden/closed
- configurable wall detail, light count, interface size, game speed, hints
- audio behind a cargo feature so native tests run (cargo test --no-default-features)
- savegame round-trip test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 23:08:33 +01:00
1 parent fbf6f223fc
commit fb7dab3683
17 files changed
+1742 -345

No files matched your search

+452 -222
View File
@@ -4,19 +4,28 @@ mod game;
mod gfx;
mod i18n;
mod map;
mod menu;
mod platform;
mod render3d;
mod save;
mod settings;
mod sound;
mod ui;
use game::Game;
use game::{Game, Msg};
use gfx::Gfx;
use macroquad::prelude::*;
use map::{idx, inb, MH, MW};
use menu::{Menu, MenuAction, Page};
use render3d::{Cam, Overlay, R3d};
use settings::{bindable_keys, Action, Settings};
use sound::SoundBank;
use ui::{hover_info, Tool, Ui, UiAction, TOOLS};
const SETTINGS_KEY: &str = "dc.settings";
const AUTOSAVE_EVERY: f32 = 120.0; // seconds of play between autosaves
const AUTOSAVE_CACHE_EVERY: f32 = 15.0; // snapshot kept in JS, written when the tab is left
fn conf() -> macroquad::conf::Conf {
macroquad::conf::Conf {
miniquad_conf: miniquad::conf::Conf {
@@ -52,215 +61,389 @@ fn next_seed(seed: u32) -> u32 {
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
}
fn apply_settings(s: &Settings, r3d: &mut R3d, sounds: &mut SoundBank) {
i18n::set_language(&s.language);
ui::set_ui_scale(s.ui_scale as f32);
r3d.outlines = s.outlines;
r3d.set_wall_detail(s.wall_subdivisions());
r3d.max_lights = s.max_lights as usize;
sounds.volume = s.volume;
sounds.muted = s.muted;
}
fn store_settings(s: &Settings) {
platform::storage_set(SETTINGS_KEY, &s.to_json());
}
// "dungeoncraft_2026-11-02_2145.dcsave" (UTC)
fn save_file_name() -> String {
let secs = miniquad::date::now() as i64;
let days = secs.div_euclid(86400);
let rem = secs.rem_euclid(86400);
// civil date from days since 1970-01-01 (Howard Hinnant's algorithm)
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
format!("dungeoncraft_{:04}-{:02}-{:02}_{:02}{:02}.dcsave", y, m, d, rem / 3600, rem % 3600 / 60)
}
struct App {
game: Game,
cam: Cam,
settings: Settings,
seed: u32,
status: Option<(String, f64)>,
}
impl App {
fn set_status(&mut self, text: String, now: f64) {
self.status = Some((text, now));
}
// Load a savegame (settings in the savegame are always applied)
fn load_bytes(&mut self, bytes: &[u8], ui: &mut Ui, menu: &mut Menu, r3d: &mut R3d, sounds: &mut SoundBank, now: f64) {
match save::from_bytes(bytes) {
Ok(l) => {
self.game = l.game;
if let Some(c) = l.camera {
self.cam = c;
}
if let Some(s) = l.settings {
self.settings = s;
apply_settings(&self.settings, r3d, sounds);
store_settings(&self.settings);
}
self.game.hints = self.settings.hints;
r3d.invalidate();
menu.page = None;
ui.show_help = false;
ui.log.clear();
ui.log.push((Msg { key: "save.loaded", args: vec![] }, now));
self.status = None;
}
Err(key) => self.set_status(i18n::tr(key), now),
}
}
fn autosave_json(&self) -> String {
save::to_json(&self.game, &self.cam, &self.settings)
}
}
#[macroquad::main(conf)]
async fn main() {
let gfx = Gfx::new();
let mut r3d = R3d::new();
let mut sounds = SoundBank::new();
sounds.generate_all().await;
let mut seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (mut game, mut cam) = new_game(seed);
let mut ui = Ui::new();
// language: stored choice, otherwise ask on first start
match platform::storage_get("dc.lang") {
Some(code) if i18n::set_language(&code) => {}
_ => ui.show_langsel = true,
let mut menu = Menu::new();
// settings: stored in the browser; on the very first start the player picks a language
let stored = platform::storage_get(SETTINGS_KEY).and_then(|s| Settings::from_json(&s));
let mut settings = stored.clone().unwrap_or_default();
if stored.is_none() {
match platform::storage_get("dc.lang") {
Some(code) => settings.language = code,
None => ui.show_langsel = true,
}
}
apply_settings(&settings, &mut r3d, &mut sounds);
ui.show_help = false;
menu.has_autosave = platform::storage_get(save::AUTOSAVE_KEY).is_some();
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (game, cam) = new_game(seed);
let mut app = App { game, cam, settings, seed, status: None };
app.game.hints = app.settings.hints;
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false };
let mut autosave_t = 0.0f32;
let mut cache_t = 0.0f32;
loop {
let dt = get_frame_time().min(0.1);
let now = get_time();
let (mx, my) = mouse_position();
let mut settings_changed = false;
// ---------------- savegame files: file picker result or dropped onto the window
let mut incoming: Option<Vec<u8>> = platform::take_file();
for f in get_dropped_files() {
if let Some(b) = f.bytes {
incoming = Some(b);
}
}
if let Some(bytes) = incoming {
app.load_bytes(&bytes, &mut ui, &mut menu, &mut r3d, &mut sounds, now);
}
// ---------------- key capture for the key binding dialog
if let Some(action) = menu.capturing() {
if is_key_pressed(KeyCode::Escape) {
menu.finish_capture();
} else if let Some(k) = get_last_key_pressed() {
if bindable_keys().any(|b| b == k) {
app.settings.bind(action, k);
apply_settings(&app.settings, &mut r3d, &mut sounds);
store_settings(&app.settings);
menu.finish_capture();
}
}
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
next_frame().await;
continue;
}
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 picked = cam.pick_tile(&game.map, mx, my);
let picked = app.cam.pick_tile(&app.game.map, mx, my);
let hover_tile = picked.map(|(x, z, _)| (x, z));
let ground = cam.ground_point(mx, my);
let ground = app.cam.ground_point(mx, my);
let (wx, wy) = (ground.x, ground.y);
let keys = app.settings.clone();
let key = |a: Action| keys.key(a);
// ---------------- keyboard
let tool_keys = [
KeyCode::Key1,
KeyCode::Key2,
KeyCode::Key3,
KeyCode::Key4,
KeyCode::Key5,
KeyCode::Key6,
KeyCode::Key7,
KeyCode::Key8,
KeyCode::Key9,
KeyCode::Key0,
KeyCode::X,
KeyCode::V,
];
for (i, k) in tool_keys.iter().enumerate() {
if is_key_pressed(*k) {
ui.tool = TOOLS[i];
inp.ldown = None;
}
}
if is_key_pressed(KeyCode::H) || (is_key_pressed(KeyCode::Escape) && ui.show_help) {
ui.show_help = !ui.show_help;
}
// ---------------- Esc: dialogs, menus, pause menu
if is_key_pressed(KeyCode::Escape) {
inp.ldown = None;
}
if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 {
game.paused = !game.paused;
}
if is_key_pressed(KeyCode::G) {
r3d.outlines = !r3d.outlines;
}
if is_key_pressed(KeyCode::M) {
sounds.muted = !sounds.muted;
}
if is_key_pressed(KeyCode::Equal) || is_key_pressed(KeyCode::KpAdd) {
sounds.change_volume(0.1);
}
if is_key_pressed(KeyCode::Minus) || is_key_pressed(KeyCode::KpSubtract) {
sounds.change_volume(-0.1);
}
if is_key_pressed(KeyCode::N) && game.state != 0 {
seed = next_seed(seed);
(game, cam) = new_game(seed);
ui.show_help = false;
if ui.show_langsel {
} else if ui.show_help {
ui.show_help = false;
} else if in_menu {
menu.back();
} else if inp.ldown.is_some() {
inp.ldown = None;
} else {
menu.page = Some(Page::Pause);
}
}
// ---------------- camera: pan
let (right, fwd) = cam.ground_axes();
let pan = cam.zoom * 0.9 * dt;
let move_cam = |dx: f32, dz: f32, cam: &mut Cam| {
let d = right * dx + fwd * dz;
cam.tx += d.x;
cam.tz += d.y;
};
if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) {
move_cam(0.0, pan, &mut cam);
}
if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) {
move_cam(0.0, -pan, &mut cam);
}
if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) {
move_cam(-pan, 0.0, &mut cam);
}
if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) {
move_cam(pan, 0.0, &mut cam);
}
let edge = 6.0;
if !ui.show_help && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
move_cam(-pan, 0.0, &mut cam);
} else if mx > screen_width() - edge {
move_cam(pan, 0.0, &mut cam);
}
if my < edge {
move_cam(0.0, pan, &mut cam);
} else if my > screen_height() - edge {
move_cam(0.0, -pan, &mut cam);
}
}
// ---------------- camera: rotate / tilt
let rot_speed = 1.8 * dt;
if is_key_down(KeyCode::Q) {
cam.yaw += rot_speed;
}
if is_key_down(KeyCode::E) {
cam.yaw -= rot_speed;
}
if is_key_down(KeyCode::R) {
cam.pitch += rot_speed * 0.6;
}
if is_key_down(KeyCode::F) {
cam.pitch -= rot_speed * 0.6;
}
if is_mouse_button_down(MouseButton::Middle) {
if let Some((lx, ly)) = inp.mlast {
cam.yaw -= (mx - lx) * 0.008;
cam.pitch += (my - ly) * 0.005;
}
inp.mlast = Some((mx, my));
} else {
inp.mlast = None;
}
cam.pitch = cam.pitch.clamp(0.35, 1.45);
// ---------------- camera: zoom around the mouse
let (_, wheel) = mouse_wheel();
if wheel != 0.0 && !over_ui {
let before = cam.ground_point(mx, my);
cam.zoom = (cam.zoom * if wheel > 0.0 { 0.88 } else { 1.0 / 0.88 }).clamp(6.0, 45.0);
let after = cam.ground_point(mx, my);
cam.tx += before.x - after.x;
cam.tz += before.y - after.y;
}
// ---------------- right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my));
inp.rlast = (mx, my);
inp.rdrag = false;
}
if let Some((sx, sy)) = inp.rdown {
if is_mouse_button_down(MouseButton::Right) {
if (mx - sx).abs() + (my - sy).abs() > 5.0 {
inp.rdrag = true;
}
if inp.rdrag {
let a = cam.ground_point(inp.rlast.0, inp.rlast.1);
let b = cam.ground_point(mx, my);
cam.tx -= b.x - a.x;
cam.tz -= b.y - a.y;
}
inp.rlast = (mx, my);
}
}
if is_mouse_button_released(MouseButton::Right) {
if !inp.rdrag && !over_ui && game.state == 0 {
if inp.ldown.is_some() {
// ---------------- keyboard (only while playing)
if playing {
for i in 0..TOOLS.len() {
if is_key_pressed(key(Action::Tool(i))) {
ui.tool = TOOLS[i];
inp.ldown = None;
} else {
game.drop(wx, wy);
}
}
inp.rdown = None;
if is_key_pressed(key(Action::Help)) {
ui.show_help = true;
}
if is_key_pressed(key(Action::Pause)) && app.game.state == 0 {
app.game.paused = !app.game.paused;
}
if is_key_pressed(key(Action::Outlines)) {
app.settings.outlines = !app.settings.outlines;
settings_changed = true;
}
if is_key_pressed(KeyCode::N) && app.game.state != 0 {
app.seed = next_seed(app.seed);
(app.game, app.cam) = new_game(app.seed);
app.game.hints = app.settings.hints;
}
}
if !dialog {
if is_key_pressed(key(Action::Mute)) {
app.settings.muted = !app.settings.muted;
settings_changed = true;
}
if is_key_pressed(key(Action::VolumeUp)) || is_key_pressed(KeyCode::KpAdd) {
app.settings.volume = (app.settings.volume + 0.1).min(1.0);
settings_changed = true;
}
if is_key_pressed(key(Action::VolumeDown)) || is_key_pressed(KeyCode::KpSubtract) {
app.settings.volume = (app.settings.volume - 0.1).max(0.0);
settings_changed = true;
}
}
// ---------------- camera
if playing {
let cam = &mut app.cam;
let (right, fwd) = cam.ground_axes();
let pan = cam.zoom * 0.9 * dt;
let move_cam = |dx: f32, dz: f32, cam: &mut Cam| {
let d = right * dx + fwd * dz;
cam.tx += d.x;
cam.tz += d.y;
};
if is_key_down(key(Action::CamForward)) || is_key_down(KeyCode::Up) {
move_cam(0.0, pan, cam);
}
if is_key_down(key(Action::CamBack)) || is_key_down(KeyCode::Down) {
move_cam(0.0, -pan, cam);
}
if is_key_down(key(Action::CamLeft)) || is_key_down(KeyCode::Left) {
move_cam(-pan, 0.0, cam);
}
if is_key_down(key(Action::CamRight)) || is_key_down(KeyCode::Right) {
move_cam(pan, 0.0, cam);
}
let edge = 6.0;
if keys.edge_scroll && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
move_cam(-pan, 0.0, cam);
} else if mx > screen_width() - edge {
move_cam(pan, 0.0, cam);
}
if my < edge {
move_cam(0.0, pan, cam);
} else if my > screen_height() - edge {
move_cam(0.0, -pan, cam);
}
}
let rot = 1.8 * dt * keys.rotate_speed;
if is_key_down(key(Action::RotateLeft)) {
cam.yaw += rot;
}
if is_key_down(key(Action::RotateRight)) {
cam.yaw -= rot;
}
if is_key_down(key(Action::TiltUp)) {
cam.pitch += rot * 0.6;
}
if is_key_down(key(Action::TiltDown)) {
cam.pitch -= rot * 0.6;
}
if is_mouse_button_down(MouseButton::Middle) {
if let Some((lx, ly)) = inp.mlast {
let inv = if keys.invert_mouse { -1.0 } else { 1.0 };
cam.yaw -= (mx - lx) * 0.008 * keys.rotate_speed * inv;
cam.pitch += (my - ly) * 0.005 * keys.rotate_speed * inv;
}
inp.mlast = Some((mx, my));
} else {
inp.mlast = None;
}
cam.pitch = cam.pitch.clamp(0.35, 1.45);
let (_, wheel) = mouse_wheel();
if wheel != 0.0 && !over_ui {
let before = cam.ground_point(mx, my);
let f = 0.88f32.powf(keys.zoom_speed);
cam.zoom = (cam.zoom * if wheel > 0.0 { f } else { 1.0 / f }).clamp(6.0, 45.0);
let after = cam.ground_point(mx, my);
cam.tx += before.x - after.x;
cam.tz += before.y - after.y;
}
// right button: drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) {
inp.rdown = Some((mx, my));
inp.rlast = (mx, my);
inp.rdrag = false;
}
if let Some((sx, sy)) = inp.rdown {
if is_mouse_button_down(MouseButton::Right) {
if (mx - sx).abs() + (my - sy).abs() > 5.0 {
inp.rdrag = true;
}
if inp.rdrag {
let a = cam.ground_point(inp.rlast.0, inp.rlast.1);
let b = cam.ground_point(mx, my);
cam.tx -= b.x - a.x;
cam.tz -= b.y - a.y;
}
inp.rlast = (mx, my);
}
}
if is_mouse_button_released(MouseButton::Right) {
if !inp.rdrag && !over_ui && app.game.state == 0 {
if inp.ldown.is_some() {
inp.ldown = None;
} else {
app.game.drop(wx, wy);
}
}
inp.rdown = None;
}
}
// ---------------- left button
if is_mouse_button_pressed(MouseButton::Left) {
match ui.click(mx, my, &game) {
UiAction::SelectTool(t) => {
ui.tool = t;
game.sfx_ui(game::Sfx::Click);
if dialog {
match ui.click(mx, my, &app.game) {
UiAction::SelectLanguage(code) => {
app.settings.language = code;
settings_changed = true;
ui.show_langsel = false;
}
UiAction::CloseHelp => ui.show_help = false,
_ => {}
}
UiAction::MinimapJump(x, y) => {
cam.tx = x;
cam.tz = y;
inp.minimap_drag = true;
} else if in_menu {
match menu.click(mx, my, &mut app.settings) {
MenuAction::SettingsChanged => settings_changed = true,
MenuAction::Resume => {}
MenuAction::NewGame => {
app.seed = next_seed(app.seed);
(app.game, app.cam) = new_game(app.seed);
app.game.hints = app.settings.hints;
menu.page = None;
ui.log.clear();
ui.show_help = true;
autosave_t = 0.0;
}
MenuAction::Continue => {
if let Some(s) = platform::storage_get(save::AUTOSAVE_KEY) {
app.load_bytes(s.as_bytes(), &mut ui, &mut menu, &mut r3d, &mut sounds, now);
}
}
MenuAction::Load => {
platform::open_file();
app.set_status(i18n::tr("save.choose"), now);
}
MenuAction::Save => {
let name = save_file_name();
platform::download(&name, app.autosave_json().as_bytes());
app.set_status(i18n::trf("save.saved", &[name]), now);
}
MenuAction::Help => ui.show_help = true,
MenuAction::MainMenu => {
if app.game.state == 0 {
platform::storage_set(save::AUTOSAVE_KEY, &app.autosave_json());
menu.has_autosave = true;
}
menu.page = Some(Page::Title);
}
MenuAction::None | MenuAction::Consumed => {}
}
UiAction::CloseHelp => ui.show_help = false,
UiAction::NewGame => {
seed = next_seed(seed);
(game, cam) = new_game(seed);
ui.show_help = false;
}
UiAction::SelectLanguage(code) => {
i18n::set_language(&code);
platform::storage_set("dc.lang", &code);
ui.show_langsel = false;
game.sfx_ui(game::Sfx::Click);
}
UiAction::Consumed => {}
UiAction::None => {
if game.state == 0 {
match (ui.tool, hover_tile) {
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&game, &cam, mx, my, true);
game.pickup(j);
} else {
match ui.click(mx, my, &app.game) {
UiAction::SelectTool(t) => {
ui.tool = t;
app.game.sfx_ui(game::Sfx::Click);
}
UiAction::MinimapJump(x, y) => {
app.cam.tx = x;
app.cam.tz = y;
inp.minimap_drag = true;
}
UiAction::NewGame => {
app.seed = next_seed(app.seed);
(app.game, app.cam) = new_game(app.seed);
app.game.hints = app.settings.hints;
}
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
UiAction::None => {
if app.game.state == 0 {
match (ui.tool, hover_tile) {
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, true);
app.game.pickup(j);
}
(Tool::Flag, Some((x, z))) => app.game.set_flag(x, z),
(Tool::Lightning, _) => app.game.lightning(wx, wy),
(Tool::Heal, _) => app.game.heal(wx, wy),
(Tool::Summon, _) => app.game.summon(wx, wy),
_ => {}
}
(Tool::Flag, Some((x, z))) => game.set_flag(x, z),
(Tool::Lightning, _) => game.lightning(wx, wy),
(Tool::Heal, _) => game.heal(wx, wy),
(Tool::Summon, _) => game.summon(wx, wy),
_ => {}
}
}
}
@@ -269,68 +452,115 @@ 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);
cam.tx = x;
cam.tz = y;
app.cam.tx = x;
app.cam.tz = y;
} else if !is_mouse_button_down(MouseButton::Left) {
inp.minimap_drag = false;
}
}
if is_mouse_button_released(MouseButton::Left) {
if let Some((sx, sy)) = inp.ldown.take() {
if let Some((ex, ey)) = hover_tile {
if let (true, Some((ex, ey))) = (playing, hover_tile) {
match ui.tool {
Tool::Dig => {
let on = !game.map.t[idx(sx, sy)].marked;
game.mark_rect(sx, sy, ex, ey, on);
let on = !app.game.map.t[idx(sx, sy)].marked;
app.game.mark_rect(sx, sy, ex, ey, on);
}
Tool::Room(r) => game.build_rect(r, sx, sy, ex, ey),
Tool::Sell => game.sell_rect(sx, sy, ex, ey),
Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
_ => {}
}
}
}
}
cam.tx = cam.tx.clamp(0.0, MW as f32);
cam.tz = cam.tz.clamp(0.0, MH as f32);
app.cam.tx = app.cam.tx.clamp(0.0, MW as f32);
app.cam.tz = app.cam.tz.clamp(0.0, MH as f32);
// ---------------- update
game.update(dt);
let hear = cam.zoom * 1.2;
for ev in game.events.drain(..) {
sounds.play(ev, vec2(cam.tx, cam.tz), hear, now);
if settings_changed {
apply_settings(&app.settings, &mut r3d, &mut sounds);
store_settings(&app.settings);
app.game.hints = app.settings.hints;
}
for m in game.msgs.drain(..) {
// ---------------- update (the world stands still while menus or dialogs are open)
if playing {
for _ in 0..(app.settings.game_speed.round().max(1.0) as usize) {
app.game.update(dt);
}
// autosave: snapshot for leaving the tab + regular autosave
if app.game.state == 0 {
cache_t += dt;
autosave_t += dt;
if cache_t >= AUTOSAVE_CACHE_EVERY {
cache_t = 0.0;
platform::cache_on_leave(save::AUTOSAVE_KEY, &app.autosave_json());
}
if autosave_t >= AUTOSAVE_EVERY {
autosave_t = 0.0;
platform::storage_set(save::AUTOSAVE_KEY, &app.autosave_json());
menu.has_autosave = true;
}
}
}
let hear = app.cam.zoom * 1.2;
for ev in app.game.events.drain(..) {
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), hear, now);
}
for m in app.game.msgs.drain(..) {
ui.log.push((m, now));
}
// ---------------- draw
clear_background(Color::from_rgba(4, 3, 5, 255));
let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None),
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
Tool::Flag => (Color::new(1.0, 0.3, 0.3, 1.0), None),
Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((wx, wy, 1.6))),
Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((wx, wy, 2.55))),
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
};
let ov = Overlay {
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
rect: inp.ldown.and_then(|(sx, sy)| hover_tile.map(|(ex, ey)| (sx, sy, ex.clamp(0, MW as i32 - 1), ey.clamp(0, MH as i32 - 1)))),
color,
radius: if over_ui { None } else { radius },
high: ui.tool == Tool::Dig,
};
r3d.draw(&game, &gfx, &cam, now, &ov);
let info = if over_ui {
Vec::new()
} else {
let ent = r3d.pick_ent(&game, &cam, mx, my, false);
hover_info(&game, ent, hover_tile)
};
ui.draw(&game, &gfx, &cam, &sounds, &info, now);
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
next_frame().await
}
}
#[allow(clippy::too_many_arguments)]
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, sounds: &SoundBank, inp: &Input, now: f64) {
let (mx, my) = mouse_position();
let blocked = menu.open() || ui.show_help || ui.show_langsel;
let over_ui = blocked || ui.over_ui(mx, my);
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));
let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None),
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
Tool::Flag => (Color::new(1.0, 0.3, 0.3, 1.0), None),
Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((ground.x, ground.y, 1.6))),
Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((ground.x, ground.y, 2.55))),
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
};
let ov = Overlay {
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
rect: inp.ldown.and_then(|(sx, sy)| hover_tile.map(|(ex, ey)| (sx, sy, ex.clamp(0, MW as i32 - 1), ey.clamp(0, MH as i32 - 1)))),
color,
radius: if over_ui { None } else { radius },
high: ui.tool == Tool::Dig,
};
r3d.draw(&app.game, gfx, &app.cam, now, &ov);
let title_screen = matches!(
menu.page,
Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })
);
if !title_screen {
let info = if over_ui {
Vec::new()
} else {
let ent = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
hover_info(&app.game, ent, hover_tile)
};
ui.draw(&app.game, gfx, &app.cam, sounds, &app.settings, &info, now);
} else {
ui.rects.clear();
}
if app.status.as_ref().map_or(false, |(_, t)| now - t > 4.0) {
app.status = None;
}
menu.draw(gfx, &app.settings, app.status.as_ref().map(|(s, _)| s.as_str()));
if !menu.open() || ui.show_help || ui.show_langsel {
ui.draw_dialogs(&app.game, gfx);
}
}