use super::{
ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, Modifier, PAD_ACTIVITY_THRESHOLD,
PadButton, Repeat, Stick,
};
use crcbl_core::input::KeyCode;
pub const CONTEXT: &str = "ui";
pub const MOVE: &str = "ui_move";
pub const NEXT: &str = "ui_next";
pub const PREV: &str = "ui_prev";
pub const ACCEPT: &str = "ui_accept";
pub const BACK: &str = "ui_back";
pub const MENU: &str = "ui_menu";
pub const ACTIONS: [&str; 6] = [MOVE, NEXT, PREV, ACCEPT, BACK, MENU];
pub const STICK_DEADZONE: f32 = PAD_ACTIVITY_THRESHOLD;
fn declarations() -> [ActionDecl; ACTIONS.len()] {
let button = |name: &str, bindings: Vec<Binding>| ActionDecl {
name: name.to_owned(),
kind: ActionKind::Button,
bindings,
};
[
ActionDecl {
name: MOVE.to_owned(),
kind: ActionKind::Axis2,
bindings: vec![
Binding::Wasd {
up: KeyCode::ArrowUp,
down: KeyCode::ArrowDown,
left: KeyCode::ArrowLeft,
right: KeyCode::ArrowRight,
},
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
},
Binding::PadStick {
stick: Stick::Left,
deadzone: STICK_DEADZONE,
},
Binding::PadDpad,
],
},
button(
NEXT,
vec![
Binding::Key(KeyCode::Tab),
Binding::PadButton(PadButton::RightShoulder),
],
),
button(
PREV,
vec![
Binding::Chord {
modifier: Modifier::Shift,
key: KeyCode::Tab,
},
Binding::PadButton(PadButton::LeftShoulder),
],
),
button(
ACCEPT,
vec![
Binding::Key(KeyCode::Enter),
Binding::Key(KeyCode::Space),
Binding::PadButton(PadButton::South),
],
),
button(
BACK,
vec![
Binding::Key(KeyCode::Escape),
Binding::PadButton(PadButton::East),
],
),
button(
MENU,
vec![
Binding::Key(KeyCode::ContextMenu),
Binding::Chord {
modifier: Modifier::Shift,
key: KeyCode::F10,
},
],
),
]
}
pub fn declare(map: &mut ActionMap) -> Result<(), ActionMapError> {
let decls = declarations();
if let Some(taken) = decls.iter().find(|decl| map.bindings(&decl.name).is_some()) {
return Err(ActionMapError::DuplicateName(taken.name.clone()));
}
for decl in decls {
map.try_declare_in(CONTEXT, decl)?;
}
for name in [MOVE, NEXT, PREV] {
map.set_repeat(name, Some(Repeat::UI))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Cardinal, GAMEPLAY_CONTEXT};
const TICK: f32 = 1.0 / 60.0;
fn declared() -> ActionMap {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "walk".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}],
});
map.declare(ActionDecl {
name: "jump".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::Space)],
});
declare(&mut map).expect("nothing reserved is taken");
map
}
#[test]
fn the_ui_context_is_declared_off_the_stack() {
let mut map = declared();
for name in ACTIONS {
assert_eq!(map.context_of(name), Some(CONTEXT), "{name}");
}
assert_eq!(
map.active_contexts().collect::<Vec<_>>(),
[GAMEPLAY_CONTEXT]
);
map.key_event(KeyCode::KeyW, true);
map.key_event(KeyCode::Space, true);
assert_eq!(map.axis2("walk"), (0.0, 1.0));
assert!(map.button_held("jump"));
assert_eq!(map.cardinal(MOVE), None);
assert!(!map.button_held(ACCEPT));
}
#[test]
fn pushed_it_drives_the_reserved_actions_from_the_table() {
let mut map = declared();
map.push_context(CONTEXT).expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::ArrowLeft, true);
assert_eq!(map.cardinal(MOVE), Some(Cardinal::Left));
map.key_event(KeyCode::ArrowLeft, false);
map.key_event(KeyCode::KeyS, true);
assert_eq!(map.cardinal(MOVE), Some(Cardinal::Down));
assert_eq!(map.axis2("walk"), (0.0, 0.0), "S is the UI's now");
map.key_event(KeyCode::KeyS, false);
for key in [KeyCode::Enter, KeyCode::Space] {
map.begin_tick(TICK);
map.key_event(key, true);
assert!(map.just_pressed(ACCEPT), "{key}");
assert!(!map.button_held("jump"));
map.key_event(key, false);
}
map.begin_tick(TICK);
map.key_event(KeyCode::Escape, true);
assert!(map.just_pressed(BACK));
map.begin_tick(TICK);
map.key_event(KeyCode::Tab, true);
assert!(map.repeated(NEXT) && !map.button_held(PREV));
map.key_event(KeyCode::Tab, false);
map.begin_tick(TICK);
map.key_event(KeyCode::ShiftRight, true);
map.key_event(KeyCode::Tab, true);
assert!(map.repeated(PREV), "Shift+Tab is ui_prev");
assert!(!map.button_held(NEXT), "and not ui_next as well");
map.key_event(KeyCode::Tab, false);
map.begin_tick(TICK);
map.key_event(KeyCode::F10, true);
assert!(map.just_pressed(MENU), "Shift+F10 is ui_menu");
map.key_event(KeyCode::F10, false);
map.key_event(KeyCode::ShiftRight, false);
map.begin_tick(TICK);
map.key_event(KeyCode::F10, true);
assert!(!map.button_held(MENU), "F10 alone is not");
map.key_event(KeyCode::F10, false);
map.begin_tick(TICK);
map.key_event(KeyCode::ContextMenu, true);
assert!(map.just_pressed(MENU), "the menu key is ui_menu");
}
#[test]
fn pushed_it_drives_the_reserved_actions_from_a_pad() {
use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PadAxis, PadKind};
let pad = GamepadId(3);
let at_rest = GamepadSnapshot::neutral(PadKind::Xbox);
let state = |snapshot| GamepadEvent::State { id: pad, snapshot };
let holding = |button: PadButton| GamepadSnapshot {
buttons: [button].into_iter().collect(),
..at_rest
};
let mut map = declared();
map.push_context(CONTEXT).expect("declared");
map.begin_tick(TICK);
let mut drift = at_rest;
drift.axes[PadAxis::LeftY as usize] = -STICK_DEADZONE / 2.0;
map.gamepad_event(&state(drift));
assert_eq!(map.cardinal(MOVE), None, "drift inside the dead zone");
let mut down = at_rest;
down.axes[PadAxis::LeftY as usize] = -1.0;
map.gamepad_event(&state(down));
assert_eq!(map.cardinal(MOVE), Some(Cardinal::Down));
assert_eq!(map.axis2("walk"), (0.0, 0.0), "the game binds no stick");
map.gamepad_event(&state(at_rest));
for (button, direction) in [
(PadButton::DpadUp, Cardinal::Up),
(PadButton::DpadDown, Cardinal::Down),
(PadButton::DpadLeft, Cardinal::Left),
(PadButton::DpadRight, Cardinal::Right),
] {
map.begin_tick(TICK);
map.gamepad_event(&state(holding(button)));
assert_eq!(map.cardinal(MOVE), Some(direction), "{button:?}");
assert!(map.repeated(MOVE), "{button:?} steps once on the press");
map.gamepad_event(&state(at_rest));
}
for (button, action) in [
(PadButton::RightShoulder, NEXT),
(PadButton::LeftShoulder, PREV),
(PadButton::South, ACCEPT),
(PadButton::East, BACK),
] {
map.begin_tick(TICK);
map.gamepad_event(&state(holding(button)));
assert!(map.just_pressed(action), "{button:?} is {action}");
map.gamepad_event(&state(at_rest));
}
}
#[test]
fn the_reserved_actions_rebind() {
let mut map = declared();
map.push_context(CONTEXT).expect("declared");
map.rebind(ACCEPT, vec![Binding::Key(KeyCode::KeyE)])
.expect("declared");
map.key_event(KeyCode::KeyE, true);
map.key_event(KeyCode::Space, true);
assert!(map.button_held(ACCEPT));
assert!(
map.button_held("jump"),
"Space is no longer the UI's, so it falls through"
);
}
#[test]
fn a_clash_declares_nothing() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: BACK.to_owned(),
kind: ActionKind::Button,
bindings: vec![],
});
assert_eq!(
declare(&mut map),
Err(ActionMapError::DuplicateName(BACK.to_owned())),
);
assert_eq!(map.action_names().collect::<Vec<_>>(), [BACK]);
}
}