use crate::input::{UiKeyCode, UiKeyEvent, UiKeyEventKind};
use flatland_client_lib::ClientKeyBindings;
pub fn key_matches(key: &UiKeyEvent, spec: &str) -> bool {
if key.kind != UiKeyEventKind::Press {
return false;
}
let parts: Vec<&str> = spec.split('+').map(str::trim).collect();
let want_shift = parts.iter().any(|p| *p == "Shift");
let want_ctrl = parts.iter().any(|p| *p == "Control" || *p == "Ctrl");
let key_part = parts.last().copied().unwrap_or("");
if key.modifiers.control != want_ctrl {
return false;
}
match key_part {
"Tab" => {
let has_shift = key.modifiers.shift || matches!(key.code, UiKeyCode::BackTab);
if has_shift != want_shift {
return false;
}
matches!(key.code, UiKeyCode::Tab | UiKeyCode::BackTab)
}
"Space" | " " => {
let has_shift = key.modifiers.shift;
if has_shift != want_shift {
return false;
}
matches!(key.code, UiKeyCode::Char(' '))
}
s if s.len() == 1 => {
let want = s.chars().next().unwrap_or('\0');
let shifted_digit = match want {
'0' => ')',
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
_ => '\0',
};
let UiKeyCode::Char(c) = key.code else {
return false;
};
let char_ok = c.eq_ignore_ascii_case(&want)
|| (want_shift && shifted_digit != '\0' && c == shifted_digit);
if !char_ok {
return false;
}
let has_shift = key.modifiers.shift
|| (want.is_ascii_alphabetic() && c.is_ascii_uppercase())
|| (want_shift && shifted_digit != '\0' && c == shifted_digit);
has_shift == want_shift
}
_ => false,
}
}
pub fn combat_action_for_key(
key: &UiKeyEvent,
keys: &ClientKeyBindings,
) -> Option<CombatKeyAction> {
if key_matches(key, &keys.target_t2) {
return Some(CombatKeyAction::CycleTargetT2 { reverse: false });
}
if key_matches(key, &keys.target_t1) {
let reverse = matches!(key.code, UiKeyCode::BackTab) || key.modifiers.shift;
return Some(CombatKeyAction::CycleTargetT1 { reverse });
}
if key_matches(key, &keys.clear_target_t2) {
return Some(CombatKeyAction::ClearTargetT2);
}
if key_matches(key, &keys.clear_target_t1) {
return Some(CombatKeyAction::ClearTargetT1);
}
if key_matches(key, &keys.auto_t2) {
return Some(CombatKeyAction::ToggleAutoT2);
}
if key_matches(key, &keys.auto_t1) {
return Some(CombatKeyAction::ToggleAutoT1);
}
if key_matches(key, &keys.loadout_menu) {
return Some(CombatKeyAction::ToggleLoadout);
}
if key_matches(key, &keys.rotation_editor) {
return Some(CombatKeyAction::ToggleRotationEditor);
}
if key_matches(key, &keys.dodge) {
return Some(CombatKeyAction::Dodge);
}
if key_matches(key, &keys.lunge) {
return Some(CombatKeyAction::Lunge);
}
if key_matches(key, &keys.block) {
return Some(CombatKeyAction::ToggleBlock);
}
if !key.modifiers.control && !key.modifiers.alt {
if let UiKeyCode::Char(c) = key.code {
if c.is_ascii_digit() && c != '0' && !key.modifiers.shift {
let slot = c.to_digit(10).unwrap_or(0) as u8;
if (1..=9).contains(&slot) {
return Some(CombatKeyAction::Hotbar(slot));
}
}
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CombatKeyAction {
CycleTargetT1 {
reverse: bool,
},
CycleTargetT2 {
reverse: bool,
},
ToggleAutoT1,
ToggleAutoT2,
ToggleLoadout,
ToggleRotationEditor,
Dodge,
Lunge,
ToggleBlock,
ClearTargetT1,
ClearTargetT2,
Hotbar(u8),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{UiKeyEventKind, UiKeyModifiers};
fn mods(shift: bool, control: bool, alt: bool) -> UiKeyModifiers {
UiKeyModifiers {
shift,
control,
alt,
}
}
fn key(code: UiKeyCode, modifiers: UiKeyModifiers) -> UiKeyEvent {
UiKeyEvent {
code,
modifiers,
kind: UiKeyEventKind::Press,
}
}
#[test]
fn shift_r_matches_uppercase_char() {
let keys = ClientKeyBindings::default();
let event = key(UiKeyCode::Char('R'), mods(true, false, false));
assert!(key_matches(&event, &keys.auto_t2));
assert_eq!(
combat_action_for_key(&event, &keys),
Some(CombatKeyAction::ToggleAutoT2)
);
}
#[test]
fn shift_r_matches_uppercase_without_shift_mod() {
let keys = ClientKeyBindings::default();
let event = key(UiKeyCode::Char('R'), mods(false, false, false));
assert_eq!(
combat_action_for_key(&event, &keys),
Some(CombatKeyAction::ToggleAutoT2)
);
}
#[test]
fn plain_r_is_t1_auto() {
let keys = ClientKeyBindings::default();
let event = key(UiKeyCode::Char('r'), mods(false, false, false));
assert_eq!(
combat_action_for_key(&event, &keys),
Some(CombatKeyAction::ToggleAutoT1)
);
}
#[test]
fn shift_r_does_not_match_t1() {
let keys = ClientKeyBindings::default();
let event = key(UiKeyCode::Char('R'), mods(true, false, false));
assert!(!key_matches(&event, &keys.auto_t1));
}
#[test]
fn dodge_c_and_block_q() {
let keys = ClientKeyBindings::default();
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('c'), mods(false, false, false)), &keys),
Some(CombatKeyAction::Dodge)
);
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('q'), mods(false, false, false)), &keys),
Some(CombatKeyAction::ToggleBlock)
);
}
#[test]
fn shift_space_is_lunge() {
let keys = ClientKeyBindings::default();
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char(' '), mods(true, false, false)), &keys),
Some(CombatKeyAction::Lunge)
);
}
#[test]
fn clear_targets_on_0_and_shift_0() {
let keys = ClientKeyBindings::default();
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('0'), mods(false, false, false)), &keys),
Some(CombatKeyAction::ClearTargetT1)
);
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('0'), mods(true, false, false)), &keys),
Some(CombatKeyAction::ClearTargetT2)
);
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char(')'), mods(true, false, false)), &keys),
Some(CombatKeyAction::ClearTargetT2)
);
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char(')'), mods(false, false, false)), &keys),
Some(CombatKeyAction::ClearTargetT2)
);
}
#[test]
fn hotbar_digits() {
let keys = ClientKeyBindings::default();
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('1'), mods(false, false, false)), &keys),
Some(CombatKeyAction::Hotbar(1))
);
assert_eq!(
combat_action_for_key(&key(UiKeyCode::Char('4'), mods(false, false, false)), &keys),
Some(CombatKeyAction::Hotbar(4))
);
}
}