// Menus: title screen, pause menu and the settings dialog (tabs, values, key bindings). // Drawn as CP437 windows on top of the (paused) game world. 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 macroquad::prelude::*; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Page { Title, Pause, Settings { tab: usize, from_title: bool }, NewGame, KeyCapture { action: usize, from_title: bool }, } pub const TABS: [&str; 5] = ["set.tab.sound", "set.tab.camera", "set.tab.graphics", "set.tab.game", "set.tab.keys"]; // One adjustable value in the settings dialog #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Item { Language, Volume, MusicVolume, Mute, RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse, Outlines, WallDetail, Lights, UiScale, GameSpeed, Hints, } fn tab_items(tab: usize) -> &'static [Item] { use Item::*; match tab { 0 => &[Language, Volume, MusicVolume, Mute], 1 => &[RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse], 2 => &[Outlines, WallDetail, Lights, UiScale], 3 => &[GameSpeed, Hints], _ => &[], } } fn item_label(it: Item) -> String { tr(match it { Item::Language => "set.language", Item::Volume => "set.volume", Item::MusicVolume => "set.music", Item::Mute => "set.mute", Item::RotateSpeed => "set.rotate_speed", Item::ZoomSpeed => "set.zoom_speed", Item::EdgeScroll => "set.edge_scroll", Item::InvertMouse => "set.invert", Item::Outlines => "set.outlines", Item::WallDetail => "set.wall_detail", Item::Lights => "set.lights", Item::UiScale => "set.ui_scale", Item::GameSpeed => "set.game_speed", Item::Hints => "set.hints", }) } fn onoff(b: bool) -> String { tr(if b { "val.on" } else { "val.off" }) } fn pct(v: f32) -> String { format!("{}%", (v * 100.0).round() as i32) } fn item_value(it: Item, s: &Settings) -> String { match it { Item::Language => languages().into_iter().find(|(c, _)| *c == s.language).map(|(_, n)| n).unwrap_or_else(|| s.language.clone()), Item::Volume => pct(s.volume), Item::MusicVolume => pct(s.music_volume), Item::Mute => onoff(s.muted), Item::RotateSpeed => pct(s.rotate_speed), Item::ZoomSpeed => pct(s.zoom_speed), Item::EdgeScroll => onoff(s.edge_scroll), Item::InvertMouse => onoff(s.invert_mouse), Item::Outlines => onoff(s.outlines), Item::WallDetail => tr(match s.wall_detail { 1 => "val.low", 3 => "val.high", _ => "val.medium", }), Item::Lights => s.max_lights.to_string(), Item::UiScale => tr(if s.ui_scale >= 3 { "val.large" } else { "val.normal" }), Item::GameSpeed => format!("{}x", s.game_speed as i32), Item::Hints => onoff(s.hints), } } fn cycle(opts: &[T], cur: T, d: i32) -> T { let i = opts.iter().position(|&o| o == cur).unwrap_or(0) as i32; opts[(i + d).rem_euclid(opts.len() as i32) as usize] } fn step(v: f32, d: i32, lo: f32, hi: f32, st: f32) -> f32 { ((v + d as f32 * st) * 100.0).round().clamp(lo * 100.0, hi * 100.0) / 100.0 } // Change a value; d = -1 / +1 (toggles flip either way) fn item_change(it: Item, s: &mut Settings, d: i32) { match it { Item::Language => { let codes: Vec = languages().into_iter().map(|(c, _)| c).collect(); let i = codes.iter().position(|c| *c == s.language).unwrap_or(0) as i32; s.language = codes[(i + d).rem_euclid(codes.len() as i32) as usize].clone(); } Item::Volume => s.volume = step(s.volume, d, 0.0, 1.0, 0.1), Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1), Item::Mute => s.muted = !s.muted, Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25), Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25), Item::EdgeScroll => s.edge_scroll = !s.edge_scroll, Item::InvertMouse => s.invert_mouse = !s.invert_mouse, 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::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d), Item::Hints => s.hints = !s.hints, } } pub fn action_label(a: Action) -> String { match a { Action::Tool(i) => tool_info(TOOLS[i]).name, other => tr(&format!("key.{}", other.id())), } } #[derive(Clone, Copy, PartialEq, Debug)] enum Btn { Continue, Resume, New, Load, Save, Settings, Help, MainMenu, Back, Tab(usize), Change(usize, i32), MapW(i32), MapH(i32), Start, Key(usize), ResetKeys, } pub enum MenuAction { None, Consumed, Continue, // load the autosave Resume, NewGame, // start a new game with the chosen map size Load, Save, Help, MainMenu, SettingsChanged, } pub struct Menu { pub page: Option, pub has_autosave: bool, buttons: Vec<(Rect, Btn)>, } fn button(gfx: &Gfx, r: Rect, label: &str, hot: bool, accent: Color) { draw_rectangle(r.x, r.y, r.w, r.h, if hot { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.13, 0.09, 0.08, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { accent }); let s = S(); gfx.text(label, r.x + (r.w - gfx.measure(label, s)) * 0.5, r.y + (r.h - GH * s) * 0.5, s, if hot { FRAME_HI } else { TXT }); } impl Menu { pub fn new() -> Menu { Menu { page: Some(Page::Title), has_autosave: false, buttons: Vec::new() } } pub fn open(&self) -> bool { self.page.is_some() } // Esc: go one level back pub fn back(&mut self) { self.page = match self.page { Some(Page::KeyCapture { from_title, .. }) => Some(Page::Settings { tab: 4, from_title }), Some(Page::Settings { from_title: true, .. }) => Some(Page::Title), Some(Page::Settings { from_title: false, .. }) => Some(Page::Pause), Some(Page::NewGame) => Some(Page::Title), Some(Page::Pause) => None, Some(Page::Title) => Some(Page::Title), None => Some(Page::Pause), }; } pub fn capturing(&self) -> Option { match self.page { Some(Page::KeyCapture { action, .. }) => all_actions().get(action).copied(), _ => None, } } pub fn finish_capture(&mut self) { if let Some(Page::KeyCapture { from_title, .. }) = self.page { self.page = Some(Page::Settings { tab: 4, from_title }); } } pub fn click(&mut self, x: f32, y: f32, settings: &mut Settings) -> MenuAction { if self.page.is_none() { return MenuAction::None; } let hit = self.buttons.iter().find(|(r, _)| r.contains(vec2(x, y))).map(|(_, b)| *b); let from_title = matches!(self.page, Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })); match hit { None => MenuAction::Consumed, Some(Btn::Continue) => MenuAction::Continue, Some(Btn::Resume) => { self.page = None; MenuAction::Resume } Some(Btn::New) => { self.page = Some(Page::NewGame); MenuAction::Consumed } Some(Btn::Start) => MenuAction::NewGame, Some(Btn::MapW(d)) => { settings.map_w = (settings.map_w as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize; MenuAction::SettingsChanged } Some(Btn::MapH(d)) => { settings.map_h = (settings.map_h as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize; MenuAction::SettingsChanged } Some(Btn::Load) => MenuAction::Load, Some(Btn::Save) => MenuAction::Save, Some(Btn::Help) => MenuAction::Help, Some(Btn::MainMenu) => MenuAction::MainMenu, Some(Btn::Settings) => { self.page = Some(Page::Settings { tab: 0, from_title }); MenuAction::Consumed } Some(Btn::Back) => { self.back(); MenuAction::Consumed } Some(Btn::Tab(t)) => { self.page = Some(Page::Settings { tab: t, from_title }); MenuAction::Consumed } Some(Btn::Change(i, d)) => { if let Some(Page::Settings { tab, .. }) = self.page { if let Some(&it) = tab_items(tab).get(i) { item_change(it, settings, d); return MenuAction::SettingsChanged; } } MenuAction::Consumed } Some(Btn::Key(a)) => { self.page = Some(Page::KeyCapture { action: a, from_title }); MenuAction::Consumed } Some(Btn::ResetKeys) => { settings.reset_keys(); MenuAction::SettingsChanged } } } 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(); 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), Page::Pause => self.draw_pause(gfx), Page::NewGame => self.draw_new_game(gfx, settings), Page::Settings { tab, .. } => self.draw_settings(gfx, settings, tab), Page::KeyCapture { action, .. } => { self.draw_settings(gfx, settings, 4); self.buttons.clear(); 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; 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"); gfx.text(&esc, x + CW() * 4.0, y + CH() * 3.0, S(), DIM); } } if let Some(st) = status { let w = gfx.measure(st, S()) + CW() * 4.0; let x = (sw - w) * 0.5; 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); gfx.text(st, x + CW() * 2.0, y + CH() * 0.5, S(), GOLD); } } fn column(&mut self, gfx: &Gfx, x: f32, y: f32, w: f32, items: &[(Btn, String)]) { let (mx, my) = mouse_position(); for (k, (b, label)) in items.iter().enumerate() { let r = Rect::new(x, y + k as f32 * CH() * 3.0, w, CH() * 2.2); 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 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 sub = tr("menu.subtitle"); gfx.text(&sub, (sw - gfx.measure(&sub, S())) * 0.5, ty + GH * big + CH(), S(), TXT); let mut items = Vec::new(); if self.has_autosave { items.push((Btn::Continue, tr("menu.continue"))); } items.push((Btn::New, tr("menu.new"))); items.push((Btn::Load, tr("menu.load"))); items.push((Btn::Settings, tr("menu.settings"))); 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); 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); 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); } // 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 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); 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); 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); 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); 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)); } // what the size means for the game let area = (s.map_w * s.map_h) as f32 / 4096.0; let portals = (area.round() as usize).max(1); let info = trf("newgame.info", &[portals.to_string(), (portals - 1).to_string()]); gfx.text(&info, x + CW() * 3.0, y + CH() * 7.5, S(), DIM); let w = (cols - 6) as f32 * CW(); self.column(gfx, x + CW() * 3.0, y + CH() * 9.5, w, &[(Btn::Start, tr("newgame.start")), (Btn::Back, tr("menu.back"))]); } fn draw_pause(&mut self, gfx: &Gfx) { let sw = screen_width(); let sh = screen_height(); let items = vec![ (Btn::Resume, tr("menu.resume")), (Btn::Save, tr("menu.save")), (Btn::Load, tr("menu.load")), (Btn::Settings, tr("menu.settings")), (Btn::Help, tr("menu.help")), (Btn::MainMenu, tr("menu.main")), ]; 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); 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 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); 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; let ty = y + CH() * 1.5; for (i, key) in TABS.iter().enumerate() { let label = tr(key); let w = gfx.measure(&label, S()) + CW() * 2.0; let r = Rect::new(tx, ty, w, CH() * 2.0); let hot = r.contains(vec2(mx, my)); draw_rectangle(r.x, r.y, r.w, r.h, if i == tab { Color::new(0.35, 0.22, 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 i == tab || hot { FRAME_HI } else { FRAME }); gfx.text(&label, r.x + CW(), r.y + CH() * 0.5, S(), if i == tab { FRAME_HI } else { TXT }); self.buttons.push((r, Btn::Tab(i))); tx += w + CW(); } let top = ty + CH() * 3.5; if tab == 4 { // key bindings in two columns 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; for (k, a) in acts.iter().enumerate() { let c = k / half; let r = k % half; let bx = x + CW() * 2.0 + c as f32 * (colw + CW() * 2.0); let by = top + r as f32 * rowh; let rect = Rect::new(bx, by - 2.0, colw, rowh - 2.0); let hot = rect.contains(vec2(mx, my)); if hot { draw_rectangle(rect.x, rect.y, rect.w, rect.h, Color::new(0.25, 0.16, 0.08, 1.0)); } let key = format!("[{}]", key_name(s.key(*a))); let maxc = ((colw - gfx.measure(&key, S()) - CW()) / CW()).max(4.0) as usize; let label: String = action_label(*a).chars().take(maxc).collect(); gfx.text(&label, bx + CW() * 0.5, by, S(), TXT); gfx.text(&key, bx + colw - gfx.measure(&key, S()) - CW() * 0.5, by, S(), GOLD); 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); 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); 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); // 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); 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); 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); button(gfx, br, &tr("menu.back"), br.contains(vec2(mx, my)), FRAME); self.buttons.push((br, Btn::Back)); } }