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