277 lines
10 KiB
Rust
277 lines
10 KiB
Rust
// 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<Action> {
|
|
let mut v: Vec<Action> = (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.<id>)
|
|
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<KeyCode> {
|
|
KEY_NAMES.iter().find(|(_, name)| *name == n).map(|(c, _)| *c)
|
|
}
|
|
|
|
// All bindable keys (for the key capture dialog)
|
|
pub fn bindable_keys() -> impl Iterator<Item = KeyCode> {
|
|
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, // interface size: 0 = automatic, 2 large, 3 medium, 4 small (interface pixels per world pixel)
|
|
pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files)
|
|
pub pixel_rows: u16, // pixel look: rows of the world on every screen (270, 360, 480)
|
|
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
|
|
// 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<String, String>,
|
|
}
|
|
|
|
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,
|
|
render_scale: 1,
|
|
pixel_rows: 360,
|
|
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,
|
|
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<Settings> {
|
|
serde_json::from_str(s).ok()
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|