1use crate::input::{UiKeyCode, UiKeyEvent, UiKeyEventKind};
4use flatland_client_lib::ClientKeyBindings;
5
6pub fn key_matches(key: &UiKeyEvent, spec: &str) -> bool {
7 if key.kind != UiKeyEventKind::Press {
8 return false;
9 }
10 let parts: Vec<&str> = spec.split('+').map(str::trim).collect();
11 let want_shift = parts.iter().any(|p| *p == "Shift");
12 let want_ctrl = parts.iter().any(|p| *p == "Control" || *p == "Ctrl");
13 let key_part = parts.last().copied().unwrap_or("");
14 if key.modifiers.control != want_ctrl {
15 return false;
16 }
17 match key_part {
18 "Tab" => {
19 let has_shift = key.modifiers.shift || matches!(key.code, UiKeyCode::BackTab);
20 if has_shift != want_shift {
21 return false;
22 }
23 matches!(key.code, UiKeyCode::Tab | UiKeyCode::BackTab)
24 }
25 "Space" | " " => {
26 let has_shift = key.modifiers.shift;
27 if has_shift != want_shift {
28 return false;
29 }
30 matches!(key.code, UiKeyCode::Char(' '))
31 }
32 s if s.len() == 1 => {
33 let want = s.chars().next().unwrap_or('\0');
34 let shifted_digit = match want {
36 '0' => ')',
37 '1' => '!',
38 '2' => '@',
39 '3' => '#',
40 '4' => '$',
41 '5' => '%',
42 '6' => '^',
43 '7' => '&',
44 '8' => '*',
45 '9' => '(',
46 _ => '\0',
47 };
48 let UiKeyCode::Char(c) = key.code else {
49 return false;
50 };
51 let char_ok = c.eq_ignore_ascii_case(&want)
52 || (want_shift && shifted_digit != '\0' && c == shifted_digit);
53 if !char_ok {
54 return false;
55 }
56 let has_shift = key.modifiers.shift
60 || (want.is_ascii_alphabetic() && c.is_ascii_uppercase())
61 || (want_shift && shifted_digit != '\0' && c == shifted_digit);
62 has_shift == want_shift
63 }
64 _ => false,
65 }
66}
67
68pub fn combat_action_for_key(
69 key: &UiKeyEvent,
70 keys: &ClientKeyBindings,
71) -> Option<CombatKeyAction> {
72 if key_matches(key, &keys.target_t2) {
73 return Some(CombatKeyAction::CycleTargetT2 { reverse: false });
74 }
75 if key_matches(key, &keys.target_t1) {
76 let reverse = matches!(key.code, UiKeyCode::BackTab) || key.modifiers.shift;
77 return Some(CombatKeyAction::CycleTargetT1 { reverse });
78 }
79 if key_matches(key, &keys.clear_target_t2) {
80 return Some(CombatKeyAction::ClearTargetT2);
81 }
82 if key_matches(key, &keys.clear_target_t1) {
83 return Some(CombatKeyAction::ClearTargetT1);
84 }
85 if key_matches(key, &keys.auto_t2) {
86 return Some(CombatKeyAction::ToggleAutoT2);
87 }
88 if key_matches(key, &keys.auto_t1) {
89 return Some(CombatKeyAction::ToggleAutoT1);
90 }
91 if key_matches(key, &keys.loadout_menu) {
92 return Some(CombatKeyAction::ToggleLoadout);
93 }
94 if key_matches(key, &keys.rotation_editor) {
95 return Some(CombatKeyAction::ToggleRotationEditor);
96 }
97 if key_matches(key, &keys.dodge) {
98 return Some(CombatKeyAction::Dodge);
99 }
100 if key_matches(key, &keys.lunge) {
101 return Some(CombatKeyAction::Lunge);
102 }
103 if key_matches(key, &keys.block) {
104 return Some(CombatKeyAction::ToggleBlock);
105 }
106 if !key.modifiers.control && !key.modifiers.alt {
108 if let UiKeyCode::Char(c) = key.code {
109 if c.is_ascii_digit() && c != '0' && !key.modifiers.shift {
110 let slot = c.to_digit(10).unwrap_or(0) as u8;
111 if (1..=9).contains(&slot) {
112 return Some(CombatKeyAction::Hotbar(slot));
113 }
114 }
115 }
116 }
117 None
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum CombatKeyAction {
122 CycleTargetT1 {
123 reverse: bool,
124 },
125 CycleTargetT2 {
126 reverse: bool,
127 },
128 ToggleAutoT1,
129 ToggleAutoT2,
130 ToggleLoadout,
131 ToggleRotationEditor,
132 Dodge,
133 Lunge,
134 ToggleBlock,
135 ClearTargetT1,
136 ClearTargetT2,
137 Hotbar(u8),
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::input::{UiKeyEventKind, UiKeyModifiers};
145
146 fn mods(shift: bool, control: bool, alt: bool) -> UiKeyModifiers {
147 UiKeyModifiers {
148 shift,
149 control,
150 alt,
151 }
152 }
153
154 fn key(code: UiKeyCode, modifiers: UiKeyModifiers) -> UiKeyEvent {
155 UiKeyEvent {
156 code,
157 modifiers,
158 kind: UiKeyEventKind::Press,
159 }
160 }
161
162 #[test]
163 fn shift_r_matches_uppercase_char() {
164 let keys = ClientKeyBindings::default();
165 let event = key(UiKeyCode::Char('R'), mods(true, false, false));
166 assert!(key_matches(&event, &keys.auto_t2));
167 assert_eq!(
168 combat_action_for_key(&event, &keys),
169 Some(CombatKeyAction::ToggleAutoT2)
170 );
171 }
172
173 #[test]
174 fn shift_r_matches_uppercase_without_shift_mod() {
175 let keys = ClientKeyBindings::default();
176 let event = key(UiKeyCode::Char('R'), mods(false, false, false));
177 assert_eq!(
178 combat_action_for_key(&event, &keys),
179 Some(CombatKeyAction::ToggleAutoT2)
180 );
181 }
182
183 #[test]
184 fn plain_r_is_t1_auto() {
185 let keys = ClientKeyBindings::default();
186 let event = key(UiKeyCode::Char('r'), mods(false, false, false));
187 assert_eq!(
188 combat_action_for_key(&event, &keys),
189 Some(CombatKeyAction::ToggleAutoT1)
190 );
191 }
192
193 #[test]
194 fn shift_r_does_not_match_t1() {
195 let keys = ClientKeyBindings::default();
196 let event = key(UiKeyCode::Char('R'), mods(true, false, false));
197 assert!(!key_matches(&event, &keys.auto_t1));
198 }
199
200 #[test]
201 fn dodge_c_and_block_q() {
202 let keys = ClientKeyBindings::default();
203 assert_eq!(
204 combat_action_for_key(&key(UiKeyCode::Char('c'), mods(false, false, false)), &keys),
205 Some(CombatKeyAction::Dodge)
206 );
207 assert_eq!(
208 combat_action_for_key(&key(UiKeyCode::Char('q'), mods(false, false, false)), &keys),
209 Some(CombatKeyAction::ToggleBlock)
210 );
211 }
212
213 #[test]
214 fn shift_space_is_lunge() {
215 let keys = ClientKeyBindings::default();
216 assert_eq!(
217 combat_action_for_key(&key(UiKeyCode::Char(' '), mods(true, false, false)), &keys),
218 Some(CombatKeyAction::Lunge)
219 );
220 }
221
222 #[test]
223 fn clear_targets_on_0_and_shift_0() {
224 let keys = ClientKeyBindings::default();
225 assert_eq!(
226 combat_action_for_key(&key(UiKeyCode::Char('0'), mods(false, false, false)), &keys),
227 Some(CombatKeyAction::ClearTargetT1)
228 );
229 assert_eq!(
230 combat_action_for_key(&key(UiKeyCode::Char('0'), mods(true, false, false)), &keys),
231 Some(CombatKeyAction::ClearTargetT2)
232 );
233 assert_eq!(
234 combat_action_for_key(&key(UiKeyCode::Char(')'), mods(true, false, false)), &keys),
235 Some(CombatKeyAction::ClearTargetT2)
236 );
237 assert_eq!(
238 combat_action_for_key(&key(UiKeyCode::Char(')'), mods(false, false, false)), &keys),
239 Some(CombatKeyAction::ClearTargetT2)
240 );
241 }
242
243 #[test]
244 fn hotbar_digits() {
245 let keys = ClientKeyBindings::default();
246 assert_eq!(
247 combat_action_for_key(&key(UiKeyCode::Char('1'), mods(false, false, false)), &keys),
248 Some(CombatKeyAction::Hotbar(1))
249 );
250 assert_eq!(
251 combat_action_for_key(&key(UiKeyCode::Char('4'), mods(false, false, false)), &keys),
252 Some(CombatKeyAction::Hotbar(4))
253 );
254 }
255}