Files
dungeoncraft/src/menu.rs
T

489 lines
20 KiB
Rust

// 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<T: PartialEq + Copy>(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<String> = 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<Page>,
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<Action> {
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));
}
}