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