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