mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Prevent GameInputs from being bound to multiple keys unless explicitly allowed. Add a Reset to Defaults button for controls.
This commit is contained in:
parent
930e0028bc
commit
8e523364ac
@ -303,6 +303,8 @@ magically infused items?"#,
|
||||
"hud.settings.audio_device": "Audio Device",
|
||||
|
||||
"hud.settings.awaitingkey": "Press a key...",
|
||||
"hud.settings.unbound": "None",
|
||||
"hud.settings.reset_keybinds": "Reset to Defaults",
|
||||
|
||||
"hud.social": "Social",
|
||||
"hud.social.online": "Online",
|
||||
|
@ -67,6 +67,7 @@ const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
||||
const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
|
||||
//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
||||
const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
|
||||
const TEXT_BIND_CONFLICT_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
|
||||
const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
||||
//const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8);
|
||||
const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
|
||||
@ -282,6 +283,7 @@ pub enum Event {
|
||||
Quit,
|
||||
ChangeLanguage(LanguageMetadata),
|
||||
ChangeBinding(GameInput),
|
||||
ResetBindings,
|
||||
ChangeFreeLookBehavior(PressBehavior),
|
||||
ChangeAutoWalkBehavior(PressBehavior),
|
||||
ChangeStopAutoWalkOnInput(bool),
|
||||
@ -1744,6 +1746,9 @@ impl Hud {
|
||||
settings_window::Event::ChangeBinding(game_input) => {
|
||||
events.push(Event::ChangeBinding(game_input));
|
||||
},
|
||||
settings_window::Event::ResetBindings => {
|
||||
events.push(Event::ResetBindings);
|
||||
},
|
||||
settings_window::Event::ChangeFreeLookBehavior(behavior) => {
|
||||
events.push(Event::ChangeFreeLookBehavior(behavior));
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar, MENU_BG,
|
||||
TEXT_COLOR,
|
||||
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar,
|
||||
ERROR_COLOR, MENU_BG, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR,
|
||||
};
|
||||
use crate::{
|
||||
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
|
||||
@ -32,6 +32,7 @@ widget_ids! {
|
||||
settings_scrollbar,
|
||||
controls_texts[],
|
||||
controls_buttons[],
|
||||
reset_controls_button,
|
||||
controls_alignment_rectangle,
|
||||
button_help,
|
||||
button_help2,
|
||||
@ -250,6 +251,7 @@ pub enum Event {
|
||||
SpeechBubbleIcon(bool),
|
||||
ChangeLanguage(LanguageMetadata),
|
||||
ChangeBinding(GameInput),
|
||||
ResetBindings,
|
||||
ChangeFreeLookBehavior(PressBehavior),
|
||||
ChangeAutoWalkBehavior(PressBehavior),
|
||||
ChangeStopAutoWalkOnInput(bool),
|
||||
@ -1512,23 +1514,25 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
|
||||
// Contents
|
||||
if let SettingsTab::Controls = self.show.settings_tab {
|
||||
// Used for sequential placement in a flow-down pattern
|
||||
let mut previous_element_id = None;
|
||||
let mut keybindings_vec: Vec<GameInput> = GameInput::iterator().collect();
|
||||
keybindings_vec.sort();
|
||||
|
||||
let controls = &self.global_state.settings.controls;
|
||||
if controls.keybindings.len() > state.ids.controls_texts.len()
|
||||
|| controls.keybindings.len() > state.ids.controls_buttons.len()
|
||||
if keybindings_vec.len() > state.ids.controls_texts.len()
|
||||
|| keybindings_vec.len() > state.ids.controls_buttons.len()
|
||||
{
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.controls_texts
|
||||
.resize(controls.keybindings.len(), &mut ui.widget_id_generator());
|
||||
.resize(keybindings_vec.len(), &mut ui.widget_id_generator());
|
||||
s.ids
|
||||
.controls_buttons
|
||||
.resize(controls.keybindings.len(), &mut ui.widget_id_generator());
|
||||
.resize(keybindings_vec.len(), &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
// Used for sequential placement in a flow-down pattern
|
||||
let mut previous_text_id = None;
|
||||
let mut keybindings_vec: Vec<&GameInput> = controls.keybindings.keys().collect();
|
||||
keybindings_vec.sort();
|
||||
|
||||
// Loop all existing keybindings and the ids for text and button widgets
|
||||
for (game_input, (&text_id, &button_id)) in keybindings_vec.into_iter().zip(
|
||||
state
|
||||
@ -1537,58 +1541,82 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.iter()
|
||||
.zip(state.ids.controls_buttons.iter()),
|
||||
) {
|
||||
if let Some(key) = controls.get_binding(*game_input) {
|
||||
let loc_key = self
|
||||
.localized_strings
|
||||
.get(game_input.get_localization_key());
|
||||
let key_string = match self.global_state.window.remapping_keybindings {
|
||||
Some(game_input_binding) => {
|
||||
if *game_input == game_input_binding {
|
||||
String::from(self.localized_strings.get("hud.settings.awaitingkey"))
|
||||
let (key_string, key_color) =
|
||||
if self.global_state.window.remapping_keybindings == Some(game_input) {
|
||||
(
|
||||
String::from(self.localized_strings.get("hud.settings.awaitingkey")),
|
||||
TEXT_COLOR,
|
||||
)
|
||||
} else if let Some(key) = controls.get_binding(game_input) {
|
||||
(
|
||||
key.to_string(),
|
||||
if controls.has_conflicting_bindings(key) {
|
||||
TEXT_BIND_CONFLICT_COLOR
|
||||
} else {
|
||||
key.to_string()
|
||||
}
|
||||
},
|
||||
None => key.to_string(),
|
||||
TEXT_COLOR
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
String::from(self.localized_strings.get("hud.settings.unbound")),
|
||||
ERROR_COLOR,
|
||||
)
|
||||
};
|
||||
|
||||
let text_widget = Text::new(loc_key)
|
||||
.color(TEXT_COLOR)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(18));
|
||||
let button_widget = Button::new()
|
||||
.label(&key_string)
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.label_font_size(self.fonts.cyri.scale(15))
|
||||
.w(150.0)
|
||||
.rgba(0.0, 0.0, 0.0, 0.0)
|
||||
.border_rgba(0.0, 0.0, 0.0, 255.0)
|
||||
.label_y(Relative::Scalar(3.0));
|
||||
// Place top-left if it's the first text, else under the previous one
|
||||
let text_widget = match previous_text_id {
|
||||
None => text_widget.top_left_with_margins_on(
|
||||
state.ids.settings_content,
|
||||
10.0,
|
||||
5.0,
|
||||
),
|
||||
Some(prev_id) => text_widget.down_from(prev_id, 10.0),
|
||||
};
|
||||
let text_width = text_widget.get_w(ui).unwrap_or(0.0);
|
||||
text_widget.set(text_id, ui);
|
||||
if button_widget
|
||||
.right_from(text_id, 350.0 - text_width)
|
||||
.set(button_id, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
events.push(Event::ChangeBinding(*game_input));
|
||||
}
|
||||
// Set the previous id to the current one for the next cycle
|
||||
previous_text_id = Some(text_id);
|
||||
let loc_key = self
|
||||
.localized_strings
|
||||
.get(game_input.get_localization_key());
|
||||
let text_widget = Text::new(loc_key)
|
||||
.color(TEXT_COLOR)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(18));
|
||||
let button_widget = Button::new()
|
||||
.label(&key_string)
|
||||
.label_color(key_color)
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.label_font_size(self.fonts.cyri.scale(15))
|
||||
.w(150.0)
|
||||
.rgba(0.0, 0.0, 0.0, 0.0)
|
||||
.border_rgba(0.0, 0.0, 0.0, 255.0)
|
||||
.label_y(Relative::Scalar(3.0));
|
||||
// Place top-left if it's the first text, else under the previous one
|
||||
let text_widget = match previous_element_id {
|
||||
None => {
|
||||
text_widget.top_left_with_margins_on(state.ids.settings_content, 10.0, 5.0)
|
||||
},
|
||||
Some(prev_id) => text_widget.down_from(prev_id, 10.0),
|
||||
};
|
||||
let text_width = text_widget.get_w(ui).unwrap_or(0.0);
|
||||
text_widget.set(text_id, ui);
|
||||
if button_widget
|
||||
.right_from(text_id, 350.0 - text_width)
|
||||
.set(button_id, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
events.push(Event::ChangeBinding(game_input));
|
||||
}
|
||||
// Set the previous id to the current one for the next cycle
|
||||
previous_element_id = Some(text_id);
|
||||
}
|
||||
if let Some(prev_id) = previous_element_id {
|
||||
let key_string = self.localized_strings.get("hud.settings.reset_keybinds");
|
||||
let button_widget = Button::new()
|
||||
.label(&key_string)
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.label_font_size(self.fonts.cyri.scale(18))
|
||||
.down_from(prev_id, 20.0)
|
||||
.w(200.0)
|
||||
.rgba(0.0, 0.0, 0.0, 0.0)
|
||||
.border_rgba(0.0, 0.0, 0.0, 255.0)
|
||||
.label_y(Relative::Scalar(3.0))
|
||||
.set(state.ids.reset_controls_button, ui);
|
||||
if button_widget.was_clicked() {
|
||||
events.push(Event::ResetBindings);
|
||||
}
|
||||
previous_element_id = Some(state.ids.reset_controls_button)
|
||||
}
|
||||
// Add an empty text widget to simulate some bottom margin, because conrod sucks
|
||||
if let Some(prev_id) = previous_text_id {
|
||||
if let Some(prev_id) = previous_element_id {
|
||||
Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT)
|
||||
.down_from(prev_id, 10.0)
|
||||
.set(state.ids.controls_alignment_rectangle, ui);
|
||||
|
@ -6,7 +6,7 @@ use crate::{
|
||||
key_state::KeyState,
|
||||
menu::char_selection::CharSelectionState,
|
||||
scene::{camera, Scene, SceneData},
|
||||
settings::AudioOutput,
|
||||
settings::{AudioOutput, ControlSettings},
|
||||
window::{AnalogGameInput, Event, GameInput},
|
||||
Direction, Error, GlobalState, PlayState, PlayStateResult,
|
||||
};
|
||||
@ -922,6 +922,10 @@ impl PlayState for SessionState {
|
||||
HudEvent::ChangeBinding(game_input) => {
|
||||
global_state.window.set_keybinding_mode(game_input);
|
||||
},
|
||||
HudEvent::ResetBindings => {
|
||||
global_state.settings.controls = ControlSettings::default();
|
||||
global_state.settings.save_to_file_warn();
|
||||
},
|
||||
HudEvent::ChangeFreeLookBehavior(behavior) => {
|
||||
global_state.settings.gameplay.free_look_behavior = behavior;
|
||||
},
|
||||
|
@ -100,9 +100,23 @@ impl ControlSettings {
|
||||
self.keybindings.insert(game_input, key_mouse);
|
||||
}
|
||||
|
||||
/// Return true if this key is used for multiple GameInputs that aren't
|
||||
/// expected to be safe to have bound to the same key at the same time
|
||||
pub fn has_conflicting_bindings(&self, key_mouse: KeyMouse) -> bool {
|
||||
if let Some(game_inputs) = self.inverse_keybindings.get(&key_mouse) {
|
||||
for a in game_inputs.iter() {
|
||||
for b in game_inputs.iter() {
|
||||
if !GameInput::can_share_bindings(*a, *b) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn default_binding(game_input: GameInput) -> KeyMouse {
|
||||
// If a new GameInput is added, be sure to update ControlSettings::default()
|
||||
// too!
|
||||
// If a new GameInput is added, be sure to update GameInput::iterator() too!
|
||||
match game_input {
|
||||
GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
|
||||
GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
|
||||
@ -157,68 +171,14 @@ impl ControlSettings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ControlSettings {
|
||||
fn default() -> Self {
|
||||
let mut new_settings = Self {
|
||||
keybindings: HashMap::new(),
|
||||
inverse_keybindings: HashMap::new(),
|
||||
};
|
||||
// Sets the initial keybindings for those GameInputs. If a new one is created in
|
||||
// future, you'll have to update default_binding, and you should update this vec
|
||||
// too.
|
||||
let game_inputs = vec![
|
||||
GameInput::Primary,
|
||||
GameInput::Secondary,
|
||||
GameInput::ToggleCursor,
|
||||
GameInput::MoveForward,
|
||||
GameInput::MoveBack,
|
||||
GameInput::MoveLeft,
|
||||
GameInput::MoveRight,
|
||||
GameInput::Jump,
|
||||
GameInput::Sit,
|
||||
GameInput::Dance,
|
||||
GameInput::Glide,
|
||||
GameInput::Climb,
|
||||
GameInput::ClimbDown,
|
||||
GameInput::Swim,
|
||||
//GameInput::WallLeap,
|
||||
GameInput::ToggleLantern,
|
||||
GameInput::Mount,
|
||||
GameInput::Enter,
|
||||
GameInput::Command,
|
||||
GameInput::Escape,
|
||||
GameInput::Map,
|
||||
GameInput::Bag,
|
||||
GameInput::Social,
|
||||
GameInput::Spellbook,
|
||||
GameInput::Settings,
|
||||
GameInput::ToggleInterface,
|
||||
GameInput::Help,
|
||||
GameInput::ToggleDebug,
|
||||
GameInput::Fullscreen,
|
||||
GameInput::Screenshot,
|
||||
GameInput::ToggleIngameUi,
|
||||
GameInput::Roll,
|
||||
GameInput::Respawn,
|
||||
GameInput::Interact,
|
||||
GameInput::ToggleWield,
|
||||
//GameInput::Charge,
|
||||
GameInput::FreeLook,
|
||||
GameInput::AutoWalk,
|
||||
GameInput::CycleCamera,
|
||||
GameInput::Slot1,
|
||||
GameInput::Slot2,
|
||||
GameInput::Slot3,
|
||||
GameInput::Slot4,
|
||||
GameInput::Slot5,
|
||||
GameInput::Slot6,
|
||||
GameInput::Slot7,
|
||||
GameInput::Slot8,
|
||||
GameInput::Slot9,
|
||||
GameInput::Slot10,
|
||||
GameInput::SwapLoadout,
|
||||
];
|
||||
for game_input in game_inputs {
|
||||
for game_input in GameInput::iterator() {
|
||||
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
|
||||
}
|
||||
new_settings
|
||||
|
@ -123,6 +123,85 @@ impl GameInput {
|
||||
GameInput::SwapLoadout => "gameinput.swaploadout",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator() -> impl Iterator<Item = GameInput> {
|
||||
[
|
||||
GameInput::Primary,
|
||||
GameInput::Secondary,
|
||||
GameInput::ToggleCursor,
|
||||
GameInput::MoveForward,
|
||||
GameInput::MoveLeft,
|
||||
GameInput::MoveRight,
|
||||
GameInput::MoveBack,
|
||||
GameInput::Jump,
|
||||
GameInput::Sit,
|
||||
GameInput::Dance,
|
||||
GameInput::Glide,
|
||||
GameInput::Climb,
|
||||
GameInput::ClimbDown,
|
||||
GameInput::Swim,
|
||||
GameInput::ToggleLantern,
|
||||
GameInput::Mount,
|
||||
GameInput::Enter,
|
||||
GameInput::Command,
|
||||
GameInput::Escape,
|
||||
GameInput::Map,
|
||||
GameInput::Bag,
|
||||
GameInput::Social,
|
||||
GameInput::Spellbook,
|
||||
GameInput::Settings,
|
||||
GameInput::ToggleInterface,
|
||||
GameInput::Help,
|
||||
GameInput::ToggleDebug,
|
||||
GameInput::Fullscreen,
|
||||
GameInput::Screenshot,
|
||||
GameInput::ToggleIngameUi,
|
||||
GameInput::Roll,
|
||||
GameInput::Respawn,
|
||||
GameInput::Interact,
|
||||
GameInput::ToggleWield,
|
||||
GameInput::FreeLook,
|
||||
GameInput::AutoWalk,
|
||||
GameInput::Slot1,
|
||||
GameInput::Slot2,
|
||||
GameInput::Slot3,
|
||||
GameInput::Slot4,
|
||||
GameInput::Slot5,
|
||||
GameInput::Slot6,
|
||||
GameInput::Slot7,
|
||||
GameInput::Slot8,
|
||||
GameInput::Slot9,
|
||||
GameInput::Slot10,
|
||||
GameInput::SwapLoadout,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Return true if `a` and `b` are able to be bound to the same key at the
|
||||
/// same time without conflict. For example, the player can't jump and climb
|
||||
/// at the same time, so these can be bound to the same key.
|
||||
pub fn can_share_bindings(a: GameInput, b: GameInput) -> bool {
|
||||
a.get_representative_binding() == b.get_representative_binding()
|
||||
}
|
||||
|
||||
/// If two GameInputs are able to be bound at the same time, then they will
|
||||
/// return the same value from this function (the representative value for
|
||||
/// that set). This models the Find operation of a disjoint-set data
|
||||
/// structure.
|
||||
fn get_representative_binding(&self) -> GameInput {
|
||||
match self {
|
||||
GameInput::Jump => GameInput::Jump,
|
||||
GameInput::Climb => GameInput::Jump,
|
||||
GameInput::Swim => GameInput::Jump,
|
||||
GameInput::Respawn => GameInput::Jump,
|
||||
|
||||
GameInput::FreeLook => GameInput::FreeLook,
|
||||
GameInput::AutoWalk => GameInput::FreeLook,
|
||||
|
||||
_ => *self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a key that the game menus recognise after input mapping
|
||||
|
Loading…
Reference in New Issue
Block a user