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