use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::actions::{ActionMap, GameAction};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, States, Hash)]
pub enum RemappingState {
#[default]
Inactive,
WaitingForInput,
}
#[derive(Debug, Clone, Default, Resource)]
pub struct RemappingContext {
pub action: Option<GameAction>,
pub timeout: f32,
pub max_timeout: f32,
}
impl RemappingContext {
pub fn start(&mut self, action: GameAction, timeout: f32) {
self.action = Some(action);
self.timeout = timeout;
self.max_timeout = timeout;
}
pub fn cancel(&mut self) {
self.action = None;
self.timeout = 0.0;
}
#[must_use]
pub fn is_active(&self) -> bool {
self.action.is_some()
}
#[must_use]
pub fn time_remaining_percent(&self) -> f32 {
if self.max_timeout > 0.0 {
self.timeout / self.max_timeout
} else {
0.0
}
}
}
#[derive(Debug, Clone, Message)]
pub struct StartRemapEvent {
pub action: GameAction,
pub timeout: f32,
}
impl StartRemapEvent {
#[must_use]
pub fn new(action: GameAction) -> Self {
Self {
action,
timeout: 5.0,
}
}
#[must_use]
pub fn with_timeout(action: GameAction, timeout: f32) -> Self {
Self { action, timeout }
}
}
#[derive(Debug, Clone, Message)]
pub enum RemapEvent {
Success {
action: GameAction,
button: GamepadButton,
},
Cancelled {
action: GameAction,
},
TimedOut {
action: GameAction,
},
Conflict {
action: GameAction,
conflicting_action: GameAction,
button: GamepadButton,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, Resource)]
pub struct SavedBindings {
#[serde(skip)]
pub gamepad: std::collections::HashMap<GameAction, Vec<GamepadButton>>,
}
impl SavedBindings {
pub fn apply_to(&self, action_map: &mut ActionMap) {
for (action, buttons) in &self.gamepad {
action_map.clear_gamepad_bindings(*action);
for button in buttons {
action_map.bind_gamepad(*action, *button);
}
}
}
pub fn save_from(&mut self, action_map: &ActionMap) {
self.gamepad.clone_from(&action_map.gamepad_bindings);
}
}
#[derive(Debug, Clone, Component)]
pub struct RemapButton {
pub action: GameAction,
}
pub fn handle_start_remap(
mut events: MessageReader<StartRemapEvent>,
mut context: ResMut<RemappingContext>,
mut next_state: ResMut<NextState<RemappingState>>,
) {
for event in events.read() {
context.start(event.action, event.timeout);
next_state.set(RemappingState::WaitingForInput);
}
}
pub fn handle_remap_input(
mut context: ResMut<RemappingContext>,
mut action_map: ResMut<ActionMap>,
mut remap_events: MessageWriter<RemapEvent>,
mut next_state: ResMut<NextState<RemappingState>>,
time: Res<Time>,
gamepads: Query<&Gamepad>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if !context.is_active() {
return;
}
let action = context.action.unwrap();
if keyboard.just_pressed(KeyCode::Escape) {
remap_events.write(RemapEvent::Cancelled { action });
context.cancel();
next_state.set(RemappingState::Inactive);
return;
}
for gamepad in gamepads.iter() {
if gamepad.just_pressed(GamepadButton::East) {
remap_events.write(RemapEvent::Cancelled { action });
context.cancel();
next_state.set(RemappingState::Inactive);
return;
}
}
for gamepad in gamepads.iter() {
let buttons_to_check = [
GamepadButton::South,
GamepadButton::North,
GamepadButton::West,
GamepadButton::LeftTrigger,
GamepadButton::RightTrigger,
GamepadButton::LeftTrigger2,
GamepadButton::RightTrigger2,
GamepadButton::LeftThumb,
GamepadButton::RightThumb,
GamepadButton::DPadUp,
GamepadButton::DPadDown,
GamepadButton::DPadLeft,
GamepadButton::DPadRight,
GamepadButton::Select,
];
for button in buttons_to_check {
if gamepad.just_pressed(button) {
let mut conflict = None;
for other_action in GameAction::all() {
if *other_action != action
&& let Some(buttons) = action_map.gamepad_bindings.get(other_action)
&& buttons.contains(&button)
{
conflict = Some(*other_action);
break;
}
}
if let Some(conflicting_action) = conflict {
remap_events.write(RemapEvent::Conflict {
action,
conflicting_action,
button,
});
} else {
action_map.clear_gamepad_bindings(action);
action_map.bind_gamepad(action, button);
remap_events.write(RemapEvent::Success { action, button });
context.cancel();
next_state.set(RemappingState::Inactive);
}
return;
}
}
}
context.timeout -= time.delta_secs();
if context.timeout <= 0.0 {
remap_events.write(RemapEvent::TimedOut { action });
context.cancel();
next_state.set(RemappingState::Inactive);
}
}
pub fn reset_bindings_to_default(mut action_map: ResMut<ActionMap>) {
*action_map = ActionMap::default();
}
pub(crate) fn add_remapping_systems(app: &mut App) {
app.init_state::<RemappingState>()
.init_resource::<RemappingContext>()
.init_resource::<SavedBindings>()
.add_message::<StartRemapEvent>()
.add_message::<RemapEvent>()
.add_systems(
Update,
(handle_start_remap, handle_remap_input)
.chain()
.run_if(in_state(RemappingState::WaitingForInput)),
)
.add_systems(Update, handle_start_remap);
}