// Player settings: stored in the browser (localStorage "dc.settings") and in every savegame. use macroquad::prelude::KeyCode; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; // Rebindable keyboard actions. Esc (menu) and the arrow keys (camera) are fixed. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Action { Tool(usize), // 0..20, same order as ui::TOOLS CamForward, CamBack, CamLeft, CamRight, RotateLeft, RotateRight, TiltUp, TiltDown, Pause, Mute, VolumeUp, VolumeDown, Outlines, Help, } pub fn all_actions() -> Vec { let mut v: Vec = (0..crate::ui::TOOLS.len()).map(Action::Tool).collect(); v.extend_from_slice(&[ Action::CamForward, Action::CamBack, Action::CamLeft, Action::CamRight, Action::RotateLeft, Action::RotateRight, Action::TiltUp, Action::TiltDown, Action::Pause, Action::Mute, Action::VolumeUp, Action::VolumeDown, Action::Outlines, Action::Help, ]); v } impl Action { // stable id used in settings files and as language key suffix (key.) pub fn id(self) -> String { match self { Action::Tool(i) => format!("tool{}", i), Action::CamForward => "cam_forward".into(), Action::CamBack => "cam_back".into(), Action::CamLeft => "cam_left".into(), Action::CamRight => "cam_right".into(), Action::RotateLeft => "rotate_left".into(), Action::RotateRight => "rotate_right".into(), Action::TiltUp => "tilt_up".into(), Action::TiltDown => "tilt_down".into(), Action::Pause => "pause".into(), Action::Mute => "mute".into(), Action::VolumeUp => "volume_up".into(), Action::VolumeDown => "volume_down".into(), Action::Outlines => "outlines".into(), Action::Help => "help".into(), } } fn default_key(self) -> KeyCode { use KeyCode::*; match self { // later tools have no key of their own (they can be bound in the settings) Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O, N].get(i).copied().unwrap_or(Unknown), Action::CamForward => W, Action::CamBack => S, Action::CamLeft => A, Action::CamRight => D, Action::RotateLeft => Q, Action::RotateRight => E, Action::TiltUp => R, Action::TiltDown => F, Action::Pause => P, Action::Mute => M, Action::VolumeUp => Equal, Action::VolumeDown => Minus, Action::Outlines => G, Action::Help => H, } } } // Keys that can be bound, with their display names const KEY_NAMES: &[(KeyCode, &str)] = &[ (KeyCode::A, "A"), (KeyCode::B, "B"), (KeyCode::C, "C"), (KeyCode::D, "D"), (KeyCode::E, "E"), (KeyCode::F, "F"), (KeyCode::G, "G"), (KeyCode::H, "H"), (KeyCode::I, "I"), (KeyCode::J, "J"), (KeyCode::K, "K"), (KeyCode::L, "L"), (KeyCode::M, "M"), (KeyCode::N, "N"), (KeyCode::O, "O"), (KeyCode::P, "P"), (KeyCode::Q, "Q"), (KeyCode::R, "R"), (KeyCode::S, "S"), (KeyCode::T, "T"), (KeyCode::U, "U"), (KeyCode::V, "V"), (KeyCode::W, "W"), (KeyCode::X, "X"), (KeyCode::Y, "Y"), (KeyCode::Z, "Z"), (KeyCode::Key0, "0"), (KeyCode::Key1, "1"), (KeyCode::Key2, "2"), (KeyCode::Key3, "3"), (KeyCode::Key4, "4"), (KeyCode::Key5, "5"), (KeyCode::Key6, "6"), (KeyCode::Key7, "7"), (KeyCode::Key8, "8"), (KeyCode::Key9, "9"), (KeyCode::Space, "Space"), (KeyCode::Minus, "-"), (KeyCode::Equal, "="), (KeyCode::Comma, ","), (KeyCode::Period, "."), (KeyCode::Slash, "/"), (KeyCode::Semicolon, ";"), (KeyCode::Apostrophe, "'"), (KeyCode::LeftBracket, "["), (KeyCode::RightBracket, "]"), (KeyCode::Backslash, "\\"), (KeyCode::Tab, "Tab"), (KeyCode::Backspace, "Backspace"), (KeyCode::Enter, "Enter"), (KeyCode::Insert, "Insert"), (KeyCode::Delete, "Delete"), (KeyCode::Home, "Home"), (KeyCode::End, "End"), (KeyCode::PageUp, "PageUp"), (KeyCode::PageDown, "PageDown"), (KeyCode::F1, "F1"), (KeyCode::F2, "F2"), (KeyCode::F3, "F3"), (KeyCode::F4, "F4"), (KeyCode::F6, "F6"), (KeyCode::F7, "F7"), (KeyCode::F8, "F8"), (KeyCode::F9, "F9"), (KeyCode::F10, "F10"), (KeyCode::Kp0, "Num0"), (KeyCode::Kp1, "Num1"), (KeyCode::Kp2, "Num2"), (KeyCode::Kp3, "Num3"), (KeyCode::Kp4, "Num4"), (KeyCode::Kp5, "Num5"), (KeyCode::Kp6, "Num6"), (KeyCode::Kp7, "Num7"), (KeyCode::Kp8, "Num8"), (KeyCode::Kp9, "Num9"), (KeyCode::KpAdd, "Num+"), (KeyCode::KpSubtract, "Num-"), (KeyCode::KpMultiply, "Num*"), (KeyCode::KpDivide, "Num/"), (KeyCode::LeftShift, "Shift"), (KeyCode::LeftControl, "Ctrl"), (KeyCode::LeftAlt, "Alt"), ]; pub fn key_name(k: KeyCode) -> String { if k == KeyCode::Unknown { return "-".into(); } KEY_NAMES.iter().find(|(c, _)| *c == k).map(|(_, n)| n.to_string()).unwrap_or_else(|| "?".into()) } fn key_from_name(n: &str) -> Option { KEY_NAMES.iter().find(|(_, name)| *name == n).map(|(c, _)| *c) } // All bindable keys (for the key capture dialog) pub fn bindable_keys() -> impl Iterator { KEY_NAMES.iter().map(|(k, _)| *k) } #[derive(Clone, Serialize, Deserialize, PartialEq)] #[serde(default)] pub struct Settings { // language & sound pub language: String, pub volume: f32, pub music_volume: f32, pub muted: bool, pub voice: bool, // Crookfinger's babbling voice pub music_track: String, // music player: "" = automatic, else the id of a calm track that replaces the calm music // camera pub rotate_speed: f32, pub pan_speed: f32, pub zoom_speed: f32, pub edge_scroll: bool, pub invert_mouse: bool, // graphics pub outlines: bool, pub wall_detail: u8, // 1 low, 2 medium, 3 high pub max_lights: u8, // 8, 16 or 24 pub ui_factor: u8, // old interface size (2 large, 3 medium, 4 small; only read to carry old settings over) pub ui_size: u16, // interface size in percent of the standard (100 = 1080 interface rows), 0 = automatic pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files) pub pixel_rows: u16, // graphics style: rows of the world on every screen (270 pixel art, 1080 high-res) pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off pub debug_fps: bool, // show FPS and rendering backend pub debug_frame_time: bool, // show the frame-time line and graph pub debug_ping: bool, // show network ping pub debug_trigger_zones: bool, // draw mission trigger zones pub debug_command_markers: bool, // draw extra command markers (guard/attack/gather stay fixed) pub debug_path_lines: bool, // draw creature path lines pub debug_holiday: u8, // holiday: 0 by the date, 1 none, 2 Halloween, 3 Christmas, 4 Easter // game pub game_speed: f32, // 1 or 2 pub hints: bool, pub difficulty: u8, // rival masters in free games: 0 easy, 1 medium, 2 hard pub map_w: usize, // size of new maps (64..256) pub match_size: usize, // versus matches: the map is match_size x match_size (see lobby::seats_for) pub map_h: usize, pub surface: bool, // new free games: the eastern half lies above ground (forest) pub events: bool, // random events in free games and versus matches // key bindings: action id -> key name pub keys: BTreeMap, } impl Default for Settings { fn default() -> Settings { Settings { language: "en".into(), volume: 0.7, music_volume: 0.6, muted: false, voice: true, music_track: String::new(), rotate_speed: 1.0, pan_speed: 1.25, zoom_speed: 1.0, edge_scroll: true, invert_mouse: false, outlines: true, wall_detail: 2, max_lights: 24, ui_factor: 0, ui_size: 0, render_scale: 1, pixel_rows: PIXEL_ART_ROWS, touch_ui: 0, debug_fps: false, debug_frame_time: false, debug_ping: false, debug_trigger_zones: false, debug_command_markers: false, debug_path_lines: false, debug_holiday: 0, game_speed: 1.0, hints: true, difficulty: 1, map_w: 64, match_size: 128, map_h: 64, surface: false, events: true, keys: BTreeMap::new(), } } } impl Settings { pub fn key(&self, a: Action) -> KeyCode { self.keys.get(&a.id()).and_then(|n| key_from_name(n)).unwrap_or_else(|| a.default_key()) } // Bind a key; if another action already uses it, the two actions swap keys pub fn bind(&mut self, a: Action, k: KeyCode) { let old = self.key(a); for other in all_actions() { if other != a && self.key(other) == k { self.keys.insert(other.id(), key_name(old)); } } self.keys.insert(a.id(), key_name(k)); } pub fn reset_keys(&mut self) { self.keys.clear(); } pub fn wall_subdivisions(&self) -> usize { match self.wall_detail { 1 => 2, 3 => 4, _ => 3, } } pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap_or_default() } pub fn from_json(s: &str) -> Option { let mut st: Settings = serde_json::from_str(s).ok()?; // old interface sizes (pixels per world pixel) become percentages if st.ui_size == 0 { st.ui_size = match st.ui_factor { 2 => 150, 3 => 100, 4 => 75, _ => 0, }; } st.ui_factor = 0; if st.ui_size != 0 && !UI_SIZES.contains(&st.ui_size) { st.ui_size = 0; } Some(st) } } // the graphics styles of the settings menu as the world's pixel rows: pixel art (270 rows, the // small hand-made sprites) and high-res (1080 rows, detailed sprites; other values from older // settings files count as pixel art) pub const PIXEL_ART_ROWS: u16 = 270; pub const HIGH_RES_ROWS: u16 = 1080; pub const STYLES: [u16; 2] = [PIXEL_ART_ROWS, HIGH_RES_ROWS]; pub fn pixel_rows(set: u16) -> u32 { if set == HIGH_RES_ROWS { HIGH_RES_ROWS as u32 } else { PIXEL_ART_ROWS as u32 } } // the interface sizes of the settings menu in percent (0 = automatic), from small to large pub const UI_SIZES: [u16; 9] = [0, 75, 85, 100, 115, 130, 150, 175, 200]; // interface rows of the virtual screen for a size in percent (100% = 1080 rows) pub fn ui_size_rows(percent: u16) -> u32 { (1080.0 * 100.0 / percent.max(1) as f32).round() as u32 } #[cfg(test)] mod tests { use super::*; #[test] fn debug_fps_round_trips() { let mut s = Settings::default(); assert!(!s.debug_fps); assert!(!s.debug_frame_time); assert!(!s.debug_ping); assert!(!s.debug_trigger_zones); assert!(!s.debug_command_markers); assert!(!s.debug_path_lines); s.debug_fps = true; s.debug_trigger_zones = true; s.debug_command_markers = true; let l = Settings::from_json(&s.to_json()).expect("settings serialize back"); assert!(l.debug_fps); assert!(l.debug_trigger_zones); assert!(l.debug_command_markers); let old = Settings::from_json(r#"{"language":"en","map_w":64,"map_h":64}"#).expect("old settings remain readable"); assert!(!old.debug_fps); assert!(!old.debug_trigger_zones); assert!(!old.debug_command_markers); } #[test] fn interface_size_percent() { let old = Settings::from_json(r#"{"ui_factor":2}"#).unwrap(); assert_eq!(old.ui_size, 150); assert_eq!(old.ui_factor, 0); let mut s = Settings::default(); s.ui_size = 115; assert_eq!(Settings::from_json(&s.to_json()).unwrap().ui_size, 115); assert_eq!(Settings::from_json(r#"{"ui_size":123}"#).unwrap().ui_size, 0); assert_eq!(ui_size_rows(100), 1080); assert_eq!(ui_size_rows(150), 720); assert!(ui_size_rows(200) < ui_size_rows(175)); } #[test] fn graphics_styles() { assert_eq!(pixel_rows(1080), 1080); assert_eq!(pixel_rows(270), 270); // older pixel-look values count as pixel art for old in [360u16, 480, 540, 0] { assert_eq!(pixel_rows(old), 270); } } }