1use flatland_client_lib::{
4 AutoNavigator, ClientKeyBindings, GameClient, PlayConnection, RotationEditorMode,
5};
6use flatland_client_ui::{
7 next_custom_preset, preset_deletable, InputAction, UiKeyCode, UiKeyEvent, UiKeyEventKind,
8 EDITOR_ABILITIES,
9};
10
11pub struct ActionCtx<'a> {
12 pub block_active: &'a mut bool,
13 pub auto_nav: &'a mut Option<AutoNavigator>,
14}
15
16pub async fn dispatch_menu_key<S: PlayConnection>(
18 client: &mut GameClient<S>,
19 key: UiKeyEvent,
20) -> bool {
21 if key.kind != UiKeyEventKind::Press {
22 return false;
23 }
24
25 if client.state.show_worker_rename {
26 match key.code {
27 UiKeyCode::Esc => client.cancel_worker_rename(),
28 UiKeyCode::Enter => {
29 if let Err(err) = client.confirm_worker_rename().await {
30 client.state.push_log(format!("Rename: {err}"));
31 }
32 }
33 UiKeyCode::Backspace => {
34 client.state.rename_buffer.pop();
35 }
36 UiKeyCode::Char(c) => {
37 if client.state.rename_buffer.chars().count() < 32 {
38 client.state.rename_buffer.push(c);
39 }
40 }
41 _ => {}
42 }
43 return true;
44 }
45
46 if client.state.show_equip_menu {
47 match key.code {
48 UiKeyCode::Char('p') | UiKeyCode::Esc => {
49 client.toggle_equip_menu();
50 }
51 UiKeyCode::Up | UiKeyCode::Char('k') => {
52 if client.state.equip_menu_index > 0 {
53 client.state.equip_menu_index -= 1;
54 }
55 }
56 UiKeyCode::Down | UiKeyCode::Char('j') => {
57 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
58 if n > 0 {
59 client.state.equip_menu_index = (client.state.equip_menu_index + 1).min(n - 1);
60 }
61 }
62 UiKeyCode::PageUp => {
63 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
64 client.state.equip_menu_index =
65 flatland_client_lib::page_list_index(client.state.equip_menu_index, -1, n);
66 }
67 UiKeyCode::PageDown => {
68 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
69 client.state.equip_menu_index =
70 flatland_client_lib::page_list_index(client.state.equip_menu_index, 1, n);
71 }
72 UiKeyCode::Enter => {
73 if let Err(err) = client.activate_equip_selection().await {
74 client.state.push_log(format!("Equip: {err}"));
75 }
76 }
77 _ => {}
78 }
79 return true;
80 }
81
82 if client.state.show_inventory_menu {
83 if client.state.show_rename_prompt {
84 match key.code {
85 UiKeyCode::Esc => client.cancel_rename_prompt(),
86 UiKeyCode::Enter => {
87 if let Err(err) = client.confirm_rename_prompt().await {
88 client.state.push_log(format!("Rename: {err}"));
89 }
90 }
91 UiKeyCode::Backspace => {
92 client.state.rename_buffer.pop();
93 }
94 UiKeyCode::Char(c) => {
95 if client.state.rename_buffer.len() < 32 {
96 client.state.rename_buffer.push(c);
97 }
98 }
99 _ => {}
100 }
101 } else if client.state.show_destroy_picker {
102 match key.code {
103 UiKeyCode::Esc => {
104 if client.state.destroy_confirm_pending {
105 client.cancel_destroy_confirm();
106 } else {
107 client.close_destroy_picker();
108 }
109 }
110 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
111 if !client.state.destroy_confirm_pending {
112 client.destroy_picker_adjust_quantity(-1);
113 }
114 }
115 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
116 if !client.state.destroy_confirm_pending {
117 client.destroy_picker_adjust_quantity(1);
118 }
119 }
120 UiKeyCode::Char('a') => {
121 if !client.state.destroy_confirm_pending {
122 client.destroy_picker_set_quantity_max();
123 }
124 }
125 UiKeyCode::Enter => {
126 if let Err(err) = client.activate_inventory_selection().await {
127 client.state.push_log(format!("Destroy failed: {err}"));
128 }
129 }
130 _ => {}
131 }
132 } else if client.state.show_grant_picker {
133 let filter_focused = client
134 .state
135 .grant_picker
136 .as_ref()
137 .map(|p| p.filter_focused)
138 .unwrap_or(false);
139 match key.code {
140 UiKeyCode::Esc => {
141 if !client.clear_or_blur_inventory_filter() {
142 client.close_grant_picker();
143 }
144 }
145 UiKeyCode::PageUp => client.inventory_menu_page(-1),
146 UiKeyCode::PageDown => client.inventory_menu_page(1),
147 UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
148 UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
149 UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
150 UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
151 client.inventory_menu_move(-1)
152 }
153 UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
154 client.inventory_menu_move(1)
155 }
156 UiKeyCode::Enter if !filter_focused => {
157 if let Err(err) = client.activate_inventory_selection().await {
158 client.state.push_log(format!("Grant failed: {err}"));
159 }
160 }
161 _ => {}
162 }
163 } else if client.state.show_move_picker {
164 let filter_focused = client
165 .state
166 .move_picker
167 .as_ref()
168 .map(|p| p.filter_focused)
169 .unwrap_or(false);
170 match key.code {
171 UiKeyCode::Esc => {
172 if !client.clear_or_blur_inventory_filter() {
173 client.close_move_picker();
174 }
175 }
176 UiKeyCode::PageUp => client.inventory_menu_page(-1),
177 UiKeyCode::PageDown => client.inventory_menu_page(1),
178 UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
179 UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
180 UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
181 UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
182 client.inventory_menu_move(-1)
183 }
184 UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
185 client.inventory_menu_move(1)
186 }
187 UiKeyCode::Char('[') | UiKeyCode::Char('-') if !filter_focused => {
188 client.move_picker_adjust_quantity(-1);
189 }
190 UiKeyCode::Char(']') | UiKeyCode::Char('=') if !filter_focused => {
191 client.move_picker_adjust_quantity(1);
192 }
193 UiKeyCode::Char('a') if !filter_focused => client.move_picker_set_quantity_max(),
194 UiKeyCode::Enter if !filter_focused => {
195 if let Err(err) = client.activate_inventory_selection().await {
196 client.state.push_log(format!("Move failed: {err}"));
197 }
198 }
199 _ => {}
200 }
201 } else if client.state.inventory_filter_focused {
202 match key.code {
203 UiKeyCode::Esc => {
204 let _ = client.clear_or_blur_inventory_filter();
205 }
206 UiKeyCode::Enter => {
207 client.state.inventory_filter_focused = false;
208 }
209 UiKeyCode::Backspace => client.inventory_filter_backspace(),
210 UiKeyCode::Char(c) => client.append_inventory_filter_char(c),
211 _ => {}
212 }
213 } else {
214 match key.code {
215 UiKeyCode::Esc => {
216 if !client.clear_or_blur_inventory_filter() {
217 client.close_inventory_menu();
218 }
219 }
220 UiKeyCode::Char('b') => client.close_inventory_menu(),
221 UiKeyCode::Tab => client.cycle_inventory_tab(true),
222 UiKeyCode::BackTab => client.cycle_inventory_tab(false),
223 UiKeyCode::PageUp => client.inventory_menu_page(-1),
224 UiKeyCode::PageDown => client.inventory_menu_page(1),
225 UiKeyCode::Char('/') => client.focus_inventory_filter(),
226 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
227 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
228 UiKeyCode::Enter => {
229 if let Err(err) = client.activate_inventory_selection().await {
230 client.state.push_log(format!("{err}"));
231 }
232 }
233 UiKeyCode::Char('m') => {
234 if let Err(err) = client.open_move_picker() {
235 client.state.push_log(format!("{err}"));
236 }
237 }
238 UiKeyCode::Char('e') => {
239 if let Err(err) = client.use_selected_consumable().await {
240 client.state.push_log(format!("Use: {err}"));
241 }
242 }
243 UiKeyCode::Char('n') => {
244 if let Err(err) = client.open_rename_prompt() {
245 client.state.push_log(format!("{err}"));
246 }
247 }
248 UiKeyCode::Char('d') => {
249 if let Err(err) = client.drop_selected().await {
250 client.state.push_log(format!("Drop: {err}"));
251 }
252 }
253 UiKeyCode::Char('x') => {
254 if let Err(err) = client.open_destroy_picker() {
255 client.state.push_log(format!("Destroy: {err}"));
256 }
257 }
258 UiKeyCode::Char('l') => {
259 if let Err(err) = client.toggle_chest_lock_for_selection().await {
260 client.state.push_log(format!("Lock: {err}"));
261 }
262 }
263 UiKeyCode::Char('g') => {
264 if let Err(err) = client.give_selected_inventory_to_worker().await {
265 client.state.push_log(format!("Give: {err}"));
266 }
267 }
268 UiKeyCode::Char('u') => {
269 if let Err(err) = client.unequip_mainhand().await {
270 client.state.push_log(format!("Unequip: {err}"));
271 }
272 }
273 _ => {}
274 }
275 }
276 return true;
277 }
278
279 if client.state.show_keychain_menu {
280 match key.code {
281 UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
282 UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
283 client.keychain_menu_move(-1);
284 }
285 UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
286 client.keychain_menu_move(1);
287 }
288 UiKeyCode::PageUp => client.keychain_menu_page(-1),
289 UiKeyCode::PageDown => client.keychain_menu_page(1),
290 UiKeyCode::Enter => {
291 if let Err(err) = client.activate_keychain_selection().await {
292 client.state.push_log(format!("Keychain: {err}"));
293 }
294 }
295 _ => {}
296 }
297 return true;
298 }
299
300 if client.state.show_craft_menu {
301 match key.code {
302 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
303 client.close_craft_menu()
304 }
305 UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
306 UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
307 UiKeyCode::PageUp => client.craft_menu_page(-1),
308 UiKeyCode::PageDown => client.craft_menu_page(1),
309 UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
310 UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
311 UiKeyCode::Char('a') => client.craft_batch_set_max(),
312 UiKeyCode::Enter => {
313 if let Err(err) = client.craft_menu_selection().await {
314 client.state.push_log(format!("Craft failed: {err}"));
315 }
316 }
317 _ => {}
318 }
319 return true;
320 }
321
322 if client.state.show_quest_offer {
323 match key.code {
324 UiKeyCode::Esc => client.quest_offer_decline(),
325 UiKeyCode::Enter => {
326 if let Err(err) = client.quest_offer_accept().await {
327 client.state.push_log(format!("Quest: {err}"));
328 }
329 }
330 _ => {}
331 }
332 return true;
333 }
334
335 if client.state.show_npc_chat {
336 match key.code {
337 UiKeyCode::Esc => {
338 if let Err(err) = client.npc_talk_close().await {
339 client.state.push_log(format!("Talk close failed: {err}"));
340 }
341 }
342 UiKeyCode::Enter => {
343 if let Err(err) = client.npc_talk_send().await {
344 client.state.push_log(format!("Talk failed: {err}"));
345 }
346 }
347 UiKeyCode::Backspace => {
348 if let Some(chat) = client.state.npc_chat.as_mut() {
349 chat.input.pop();
350 }
351 }
352 UiKeyCode::Char(c) => {
353 if let Some(chat) = client.state.npc_chat.as_mut() {
354 if !chat.pending && chat.input.len() < 300 {
355 chat.input.push(c);
356 }
357 }
358 }
359 _ => {}
360 }
361 return true;
362 }
363
364 if client.state.show_npc_verb_menu {
365 match key.code {
366 UiKeyCode::Esc => {
367 client.state.show_npc_verb_menu = false;
368 client.state.npc_verb_target = None;
369 }
370 UiKeyCode::Up | UiKeyCode::Char('k') => {
371 if client.state.npc_verb_index > 0 {
372 client.state.npc_verb_index -= 1;
373 }
374 }
375 UiKeyCode::Down | UiKeyCode::Char('j') => {
376 let max = client.npc_verb_options().len().saturating_sub(1);
377 if client.state.npc_verb_index < max {
378 client.state.npc_verb_index += 1;
379 }
380 }
381 UiKeyCode::Enter => {
382 if let Err(err) = client.confirm_npc_verb().await {
383 client.state.push_log(format!("Interact failed: {err}"));
384 }
385 }
386 _ => {}
387 }
388 return true;
389 }
390
391 if client.state.show_shop_menu {
392 match key.code {
393 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
394 if let Err(err) = client.back_from_shop_menu().await {
395 client.state.push_log(format!("Shop close failed: {err}"));
396 }
397 }
398 UiKeyCode::Tab => client.shop_tab_toggle(),
399 UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
400 UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
401 UiKeyCode::PageUp => client.shop_menu_page(-1),
402 UiKeyCode::PageDown => client.shop_menu_page(1),
403 UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
404 UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
405 UiKeyCode::Char('a') => client.shop_quantity_set_max(),
406 UiKeyCode::Enter => {
407 if let Err(err) = client.shop_confirm().await {
408 client.state.push_log(format!("Trade failed: {err}"));
409 }
410 }
411 _ => {}
412 }
413 return true;
414 }
415
416 if client.state.show_quest_menu {
417 match key.code {
418 UiKeyCode::Esc => {
419 if client.state.quest_withdraw_confirm {
420 client.state.quest_withdraw_confirm = false;
421 } else {
422 client.state.show_quest_menu = false;
423 }
424 }
425 UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
426 UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
427 UiKeyCode::PageUp => client.quest_menu_page(-1),
428 UiKeyCode::PageDown => client.quest_menu_page(1),
429 UiKeyCode::Char('x') => client.quest_request_withdraw(),
430 UiKeyCode::Enter => {
431 if let Err(err) = client.quest_confirm_action().await {
432 client.state.push_log(format!("Quest: {err}"));
433 }
434 }
435 _ => {}
436 }
437 return true;
438 }
439
440 if client.state.worker_route_editor.is_some() {
441 let at_root = client.re_at_root_sheet();
442 let sheet_index = client.re_sheet_index();
443 if at_root {
444 match key.code {
446 UiKeyCode::Esc => client.close_worker_route_editor(),
447 UiKeyCode::Char('s') => {
448 if let Err(err) = client.worker_route_editor_save().await {
449 client.state.push_log(format!("Route: {err}"));
450 }
451 }
452 UiKeyCode::Down => {
453 if key.modifiers.contains_control() {
454 client.worker_route_editor_move_selected(1);
455 } else {
456 client.worker_route_editor_select(1);
457 }
458 }
459 UiKeyCode::Up => {
460 if key.modifiers.contains_control() {
461 client.worker_route_editor_move_selected(-1);
462 } else {
463 client.worker_route_editor_select(-1);
464 }
465 }
466 UiKeyCode::Char('j') => {
469 if key.modifiers.contains_control() {
470 client.worker_route_editor_move_selected(-1);
471 } else {
472 client.worker_route_editor_select(1);
473 }
474 }
475 UiKeyCode::Char('k') => {
476 if key.modifiers.contains_control() {
477 client.worker_route_editor_move_selected(1);
478 } else {
479 client.worker_route_editor_select(-1);
480 }
481 }
482 UiKeyCode::Char('d') | UiKeyCode::Delete => {
483 client.worker_route_editor_delete_selected()
484 }
485 UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
486 UiKeyCode::Char('a') => client.re_open_add_menu(),
487 UiKeyCode::Char('l') => client.re_open_bed_picker(),
488 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
489 UiKeyCode::Enter => client.re_edit_selected_stop(),
490 _ => {}
491 }
492 } else {
493 match key.code {
495 UiKeyCode::Esc => client.re_sheet_back(),
496 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
497 UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
498 UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
499 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
500 client.re_sheet_row_activate(sheet_index)
501 }
502 UiKeyCode::Char('[') | UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
503 UiKeyCode::Char(']') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
504 _ => {}
505 }
506 }
507 return true;
508 }
509
510 if client.state.show_worker_give_picker {
511 match key.code {
512 UiKeyCode::Esc => client.close_worker_give_picker(),
513 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
514 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
515 UiKeyCode::Enter => {
516 if let Err(err) = client.confirm_worker_give_picker().await {
517 client.state.push_log(format!("Give: {err}"));
518 }
519 }
520 _ => {}
521 }
522 return true;
523 }
524
525 if client.state.show_worker_give_target_picker {
526 match key.code {
527 UiKeyCode::Esc => client.close_worker_give_target_picker(),
528 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_target_picker_move(-1),
529 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_target_picker_move(1),
530 UiKeyCode::Enter => {
531 if let Err(err) = client.confirm_worker_give_target_picker().await {
532 client.state.push_log(format!("Give: {err}"));
533 }
534 }
535 _ => {}
536 }
537 return true;
538 }
539
540 if client.state.show_worker_take_picker {
541 match key.code {
542 UiKeyCode::Esc => client.close_worker_take_picker(),
543 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_take_picker_move(-1),
544 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_take_picker_move(1),
545 UiKeyCode::Enter => {
546 if let Err(err) = client.confirm_worker_take_picker().await {
547 client.state.push_log(format!("Take: {err}"));
548 }
549 }
550 _ => {}
551 }
552 return true;
553 }
554
555 if client.state.show_worker_teach_picker {
556 match key.code {
557 UiKeyCode::Esc => client.close_worker_teach_picker(),
558 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
559 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
560 UiKeyCode::Enter => {
561 if let Err(err) = client.confirm_worker_teach_picker().await {
562 client.state.push_log(format!("Teach: {err}"));
563 }
564 }
565 _ => {}
566 }
567 return true;
568 }
569
570 if client.state.show_workers_menu {
571 match key.code {
572 UiKeyCode::Esc => client.state.show_workers_menu = false,
573 UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
574 UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
575 UiKeyCode::PageUp => client.workers_menu_page(-1),
576 UiKeyCode::PageDown => client.workers_menu_page(1),
577 UiKeyCode::Char('d') => {
578 if let Err(err) = client.workers_dismiss_selected().await {
579 client.state.push_log(format!("Worker: {err}"));
580 }
581 }
582 UiKeyCode::Char('r') => {
583 if let Err(err) = client.hire_worker_laborer().await {
584 client.state.push_log(format!("Hire: {err}"));
585 }
586 }
587 UiKeyCode::Char('e') => {
588 if let Err(err) = client.open_worker_route_editor_for_selected() {
589 client.state.push_log(format!("Route: {err}"));
590 }
591 }
592 UiKeyCode::Char('n') => {
593 if let Err(err) = client.open_worker_rename() {
594 client.state.push_log(format!("Rename: {err}"));
595 }
596 }
597 UiKeyCode::Char('g') => {
598 if let Err(err) = client.open_worker_give_picker() {
599 client.state.push_log(format!("Give: {err}"));
600 }
601 }
602 UiKeyCode::Char('i') => {
603 if let Err(err) = client.open_worker_take_picker() {
604 client.state.push_log(format!("Take: {err}"));
605 }
606 }
607 UiKeyCode::Char('t') => {
608 if let Err(err) = client.open_worker_teach_picker() {
609 client.state.push_log(format!("Teach: {err}"));
610 }
611 }
612 UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
613 UiKeyCode::Enter => {
614 if let Err(err) = client.workers_confirm_action().await {
615 client.state.push_log(format!("Worker: {err}"));
616 }
617 }
618 _ => {}
619 }
620 return true;
621 }
622
623 false
624}
625
626pub async fn dispatch_action<S: PlayConnection>(
627 client: &mut GameClient<S>,
628 keys: &ClientKeyBindings,
629 action: InputAction,
630 ctx: &mut ActionCtx<'_>,
631) {
632 match action {
633 InputAction::Harvest => {
634 if let Err(err) = client.harvest_nearest().await {
635 client.state.push_log(format!("Harvest failed: {err}"));
636 }
637 }
638 InputAction::Pickup => {
639 if client.pickup_nearest().await.is_err() {
640 if let Err(err) = client.pickup_nearest_container().await {
641 client.state.push_log(format!("Pickup: {err}"));
642 }
643 }
644 }
645 InputAction::Craft => client.open_craft_menu(),
646 InputAction::Interact | InputAction::UseWorld => {
647 if let Err(err) = client.use_nearest().await {
648 client.state.push_log(format!("Use: {err}"));
649 }
650 }
651 InputAction::TestDamage => {
652 if let Err(err) = client.test_damage(25.0).await {
653 client.state.push_log(format!("Damage failed: {err}"));
654 }
655 }
656 InputAction::CycleCombatTarget { reverse } => {
657 if let Err(err) = client.cycle_combat_target(reverse).await {
658 client.state.push_log(format!("T1 target: {err}"));
659 }
660 }
661 InputAction::CycleCombatTargetT2 { reverse } => {
662 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
663 client.state.push_log(format!("T2 target: {err}"));
664 }
665 }
666 InputAction::AdvanceRotationT1 => {
667 if let Err(err) = client.advance_rotation(1).await {
668 client.state.push_log(format!("T1 step: {err}"));
669 }
670 }
671 InputAction::AdvanceRotationT2 => {
672 if let Err(err) = client.advance_rotation(2).await {
673 client.state.push_log(format!("T2 step: {err}"));
674 }
675 }
676 InputAction::ToggleAutoT1 => {
677 if let Err(err) = client.toggle_auto_attack_slot(1).await {
678 client.state.push_log(format!("T1 auto: {err}"));
679 }
680 }
681 InputAction::ToggleAutoT2 => {
682 if let Err(err) = client.toggle_auto_attack_slot(2).await {
683 client.state.push_log(format!("T2 auto: {err}"));
684 }
685 }
686 InputAction::ToggleLoadout => {
687 client.state.show_rotation_editor = false;
688 client.state.rotation_editor.reset();
689 client.state.show_loadout_menu = !client.state.show_loadout_menu;
690 if client.state.show_loadout_menu {
691 *ctx.auto_nav = None;
692 let _ = client.stop().await;
693 }
694 }
695 InputAction::ToggleRotationEditor => {
696 client.state.show_loadout_menu = false;
697 if client.state.show_rotation_editor {
698 client.state.show_rotation_editor = false;
699 client.state.rotation_editor.reset();
700 } else {
701 client.state.show_rotation_editor = true;
702 client.state.rotation_editor.reset();
703 let max = client.state.rotation_presets.len();
704 if max > 0 {
705 client.state.rotation_editor.list_index =
706 client.state.rotation_editor.list_index.min(max - 1);
707 }
708 }
709 if client.state.show_rotation_editor {
710 *ctx.auto_nav = None;
711 let _ = client.stop().await;
712 }
713 }
714 InputAction::LoadoutMenuUp => {
715 if client.state.show_loadout_menu && client.state.loadout_menu_index > 0 {
716 client.state.loadout_menu_index -= 1;
717 }
718 }
719 InputAction::LoadoutMenuDown => {
720 if client.state.show_loadout_menu {
721 let max = client.state.rotation_presets.len();
722 if max > 0 {
723 client.state.loadout_menu_index =
724 (client.state.loadout_menu_index + 1).min(max - 1);
725 }
726 }
727 }
728 InputAction::LoadoutAssignT1 => {
729 let preset_id = {
730 let idx = client.state.loadout_menu_index;
731 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
732 };
733 if let Some(preset_id) = preset_id {
734 if let Err(err) = client.assign_slot_preset(1, &preset_id).await {
735 client.state.push_log(format!("Loadout: {err}"));
736 }
737 }
738 }
739 InputAction::LoadoutAssignT2 => {
740 let preset_id = {
741 let idx = client.state.loadout_menu_index;
742 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
743 };
744 if let Some(preset_id) = preset_id {
745 if let Err(err) = client.assign_slot_preset(2, &preset_id).await {
746 client.state.push_log(format!("Loadout: {err}"));
747 }
748 }
749 }
750 InputAction::CloseOverlay => {
751 client.state.show_loadout_menu = false;
752 client.state.show_rotation_editor = false;
753 client.state.rotation_editor.reset();
754 }
755 InputAction::ClearCombatTarget => {
756 if let Err(err) = client.clear_combat_target().await {
757 client.state.push_log(format!("Target: {err}"));
758 }
759 }
760 InputAction::ClearCombatTargetT2 => {
761 if let Err(err) = client.clear_combat_target_slot(2).await {
762 client.state.push_log(format!("T2 target: {err}"));
763 }
764 }
765 InputAction::Dodge => {
766 *ctx.auto_nav = None;
767 if let Err(err) = client.dodge().await {
768 client.state.push_log(format!("Dodge: {err}"));
769 }
770 }
771 InputAction::Lunge => {
772 *ctx.auto_nav = None;
773 let _ = client.stop().await;
774 if let Err(err) = client.lunge().await {
775 client.state.push_log(format!("Lunge: {err}"));
776 }
777 }
778 InputAction::DirectionalJump { forward, strafe } => {
779 *ctx.auto_nav = None;
780 let _ = client.stop().await;
781 if let Err(err) = client.directional_jump(forward, strafe).await {
782 client.state.push_log(format!("Jump: {err}"));
783 }
784 }
785 InputAction::CastHotbar { slot } => match keys.hotbar_ability(slot) {
786 Some(ability_id) => {
787 let ability_id = ability_id.to_string();
788 if let Err(err) = client.cast_hotbar_ability(&ability_id).await {
789 client.state.push_log(format!("Hotbar {slot}: {err}"));
790 }
791 }
792 None => {
793 client.state.push_log(format!(
794 "Hotbar {slot} unbound — set in client-settings.yaml"
795 ));
796 }
797 },
798 InputAction::ToggleBlock => {
799 *ctx.block_active = !*ctx.block_active;
800 if let Err(err) = client.set_block(*ctx.block_active).await {
801 client.state.push_log(format!("Block: {err}"));
802 *ctx.block_active = false;
803 }
804 }
805 InputAction::ToggleStats => client.toggle_stats(),
806 InputAction::ToggleEquip => client.toggle_equip_menu(),
807 InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
808 InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
809 InputAction::ToggleInventory => client.toggle_inventory_menu(),
810 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
811 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
812 InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
813 InputAction::QuestMenuUp => client.quest_menu_move(-1),
814 InputAction::QuestMenuDown => client.quest_menu_move(1),
815 InputAction::QuestWithdraw => client.quest_request_withdraw(),
816 InputAction::RotationEditorBack => {
817 let _ = client.back_on_esc();
818 }
819 InputAction::RotationEditorListUp
820 | InputAction::RotationEditorListDown
821 | InputAction::RotationEditorEdit
822 | InputAction::RotationEditorNew
823 | InputAction::RotationEditorDelete
824 | InputAction::RotationEditorAddAbility
825 | InputAction::RotationEditorRemoveAbility
826 | InputAction::RotationEditorMoveAbilityUp
827 | InputAction::RotationEditorMoveAbilityDown
828 | InputAction::RotationEditorAbilityUp
829 | InputAction::RotationEditorAbilityDown
830 | InputAction::RotationEditorPickerUp
831 | InputAction::RotationEditorPickerDown
832 | InputAction::RotationEditorPickAbility
833 | InputAction::RotationEditorRename
834 | InputAction::RotationEditorConfirmLabel
835 | InputAction::RotationEditorLabelBackspace
836 | InputAction::RotationEditorLabelChar(_)
837 | InputAction::RotationEditorSave => {
838 handle_rotation_editor_action(client, action).await;
839 }
840 InputAction::None
841 | InputAction::Quit
842 | InputAction::ToggleHelp
843 | InputAction::CycleHudView
844 | InputAction::StartChat { .. }
845 | InputAction::SubmitChat
846 | InputAction::CancelChat
847 | InputAction::ToggleSprintMode
848 | InputAction::ToggleMapTarget
849 | InputAction::ConfirmMapTarget
850 | InputAction::CancelMapTarget
851 | InputAction::MapTargetNudge { .. }
852 | InputAction::CancelAutoNav
853 | InputAction::StopMovement => {}
854 }
855}
856
857async fn handle_rotation_editor_action<S: PlayConnection>(
858 client: &mut GameClient<S>,
859 action: InputAction,
860) {
861 match action {
862 InputAction::RotationEditorListUp => {
863 if client.state.rotation_editor.list_index > 0 {
864 client.state.rotation_editor.list_index -= 1;
865 }
866 }
867 InputAction::RotationEditorListDown => {
868 let max = client.state.rotation_presets.len();
869 if max > 0 {
870 client.state.rotation_editor.list_index =
871 (client.state.rotation_editor.list_index + 1).min(max - 1);
872 }
873 }
874 InputAction::RotationEditorEdit => {
875 let idx = client.state.rotation_editor.list_index;
876 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
877 client.state.rotation_editor.draft = Some(preset);
878 client.state.rotation_editor.ability_index = 0;
879 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
880 }
881 }
882 InputAction::RotationEditorNew => {
883 let preset = next_custom_preset(&client.state.rotation_presets);
884 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
885 client.state.rotation_editor.draft = Some(preset);
886 client.state.rotation_editor.ability_index = 0;
887 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
888 }
889 InputAction::RotationEditorDelete => {
890 let idx = client.state.rotation_editor.list_index;
891 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
892 if let Some(id) = preset_id {
893 if preset_deletable(&id) {
894 if let Err(err) = client.delete_rotation_preset(&id).await {
895 client.state.push_log(format!("Delete: {err}"));
896 } else {
897 let max = client.state.rotation_presets.len();
898 client.state.rotation_editor.list_index = if max == 0 {
899 0
900 } else {
901 client.state.rotation_editor.list_index.min(max - 1)
902 };
903 }
904 } else {
905 client.state.push_log("Cannot delete built-in preset");
906 }
907 }
908 }
909 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
910 RotationEditorMode::EditLabel => {
911 client.state.rotation_editor.label_buffer.clear();
912 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
913 }
914 RotationEditorMode::PickAbility => {
915 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
916 }
917 RotationEditorMode::EditSequence => {
918 client.state.rotation_editor.draft = None;
919 client.state.rotation_editor.mode = RotationEditorMode::List;
920 }
921 RotationEditorMode::List => {}
922 },
923 InputAction::RotationEditorAddAbility => {
924 client.state.rotation_editor.picker_index = 0;
925 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
926 }
927 InputAction::RotationEditorRemoveAbility => {
928 let idx = client.state.rotation_editor.ability_index;
929 if let Some(draft) = &mut client.state.rotation_editor.draft {
930 if idx < draft.abilities.len() {
931 draft.abilities.remove(idx);
932 if client.state.rotation_editor.ability_index >= draft.abilities.len()
933 && client.state.rotation_editor.ability_index > 0
934 {
935 client.state.rotation_editor.ability_index -= 1;
936 }
937 }
938 }
939 }
940 InputAction::RotationEditorMoveAbilityUp => {
941 let i = client.state.rotation_editor.ability_index;
942 if let Some(draft) = &mut client.state.rotation_editor.draft {
943 if i > 0 && i < draft.abilities.len() {
944 draft.abilities.swap(i, i - 1);
945 client.state.rotation_editor.ability_index -= 1;
946 }
947 }
948 }
949 InputAction::RotationEditorMoveAbilityDown => {
950 let i = client.state.rotation_editor.ability_index;
951 if let Some(draft) = &mut client.state.rotation_editor.draft {
952 if i + 1 < draft.abilities.len() {
953 draft.abilities.swap(i, i + 1);
954 client.state.rotation_editor.ability_index += 1;
955 }
956 }
957 }
958 InputAction::RotationEditorAbilityUp => {
959 if client.state.rotation_editor.ability_index > 0 {
960 client.state.rotation_editor.ability_index -= 1;
961 }
962 }
963 InputAction::RotationEditorAbilityDown => {
964 if let Some(draft) = &client.state.rotation_editor.draft {
965 if !draft.abilities.is_empty() {
966 client.state.rotation_editor.ability_index =
967 (client.state.rotation_editor.ability_index + 1)
968 .min(draft.abilities.len() - 1);
969 }
970 }
971 }
972 InputAction::RotationEditorPickerUp => {
973 if client.state.rotation_editor.picker_index > 0 {
974 client.state.rotation_editor.picker_index -= 1;
975 }
976 }
977 InputAction::RotationEditorPickerDown => {
978 if !EDITOR_ABILITIES.is_empty() {
979 client.state.rotation_editor.picker_index =
980 (client.state.rotation_editor.picker_index + 1).min(EDITOR_ABILITIES.len() - 1);
981 }
982 }
983 InputAction::RotationEditorPickAbility => {
984 let pick = client.state.rotation_editor.picker_index;
985 if let Some(ability) = EDITOR_ABILITIES.get(pick) {
986 if let Some(draft) = &mut client.state.rotation_editor.draft {
987 draft.abilities.push((*ability).to_string());
988 client.state.rotation_editor.ability_index =
989 draft.abilities.len().saturating_sub(1);
990 }
991 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
992 }
993 }
994 InputAction::RotationEditorRename => {
995 if let Some(draft) = &client.state.rotation_editor.draft {
996 client.state.rotation_editor.label_buffer = draft.label.clone();
997 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
998 }
999 }
1000 InputAction::RotationEditorConfirmLabel => {
1001 let label = client.state.rotation_editor.label_buffer.trim().to_string();
1002 if label.is_empty() {
1003 client.state.push_log("Label cannot be empty");
1004 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1005 draft.label = label;
1006 client.state.rotation_editor.label_buffer.clear();
1007 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1008 }
1009 }
1010 InputAction::RotationEditorLabelBackspace => {
1011 client.state.rotation_editor.label_buffer.pop();
1012 }
1013 InputAction::RotationEditorLabelChar(c) => {
1014 if client.state.rotation_editor.label_buffer.len() < 32 {
1015 client.state.rotation_editor.label_buffer.push(c);
1016 }
1017 }
1018 InputAction::RotationEditorSave => {
1019 let draft = match client.state.rotation_editor.draft.clone() {
1020 Some(d) => d,
1021 None => return,
1022 };
1023 if draft.abilities.is_empty() {
1024 client.state.push_log("Rotation needs at least one ability");
1025 return;
1026 }
1027 let saved_id = draft.id.clone();
1028 if let Err(err) = client.upsert_rotation_preset(draft).await {
1029 client.state.push_log(format!("Save: {err}"));
1030 } else {
1031 client.state.rotation_editor.mode = RotationEditorMode::List;
1032 client.state.rotation_editor.draft = None;
1033 if let Some(i) = client
1034 .state
1035 .rotation_presets
1036 .iter()
1037 .position(|p| p.id == saved_id)
1038 {
1039 client.state.rotation_editor.list_index = i;
1040 }
1041 }
1042 }
1043 _ => {}
1044 }
1045}