Skip to main content

flatland_play_loop/
actions.rs

1//! Input action and overlay menu dispatch for any [`GameClient`] session.
2
3use 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
16/// Returns true if the key was consumed by a menu handler.
17pub 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            // Root: the ordered stop list.
327            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                // Select: vim j/k. Reorder: Ctrl+j = earlier (up), Ctrl+k = later (down)
349                // — matches "k moves down" and keeps arrow+Ctrl natural.
350                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            // Inside a setup sheet: uniform picker keys.
376            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_teach_picker {
408        match key.code {
409            UiKeyCode::Esc => client.close_worker_teach_picker(),
410            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
411            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
412            UiKeyCode::Enter => {
413                if let Err(err) = client.confirm_worker_teach_picker().await {
414                    client.state.push_log(format!("Teach: {err}"));
415                }
416            }
417            _ => {}
418        }
419        return true;
420    }
421
422    if client.state.show_workers_menu {
423        match key.code {
424            UiKeyCode::Esc => client.state.show_workers_menu = false,
425            UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
426            UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
427            UiKeyCode::Char('d') => {
428                if let Err(err) = client.workers_dismiss_selected().await {
429                    client.state.push_log(format!("Worker: {err}"));
430                }
431            }
432            UiKeyCode::Char('r') => {
433                if let Err(err) = client.hire_worker_laborer().await {
434                    client.state.push_log(format!("Hire: {err}"));
435                }
436            }
437            UiKeyCode::Char('e') => {
438                if let Err(err) = client.open_worker_route_editor_for_selected() {
439                    client.state.push_log(format!("Route: {err}"));
440                }
441            }
442            UiKeyCode::Char('n') => {
443                if let Err(err) = client.open_worker_rename() {
444                    client.state.push_log(format!("Rename: {err}"));
445                }
446            }
447            UiKeyCode::Char('g') => {
448                if let Err(err) = client.open_worker_give_picker() {
449                    client.state.push_log(format!("Give: {err}"));
450                }
451            }
452            UiKeyCode::Char('t') => {
453                if let Err(err) = client.open_worker_teach_picker() {
454                    client.state.push_log(format!("Teach: {err}"));
455                }
456            }
457            UiKeyCode::Enter => {
458                if let Err(err) = client.workers_confirm_action().await {
459                    client.state.push_log(format!("Worker: {err}"));
460                }
461            }
462            _ => {}
463        }
464        return true;
465    }
466
467    false
468}
469
470pub async fn dispatch_action<S: PlayConnection>(
471    client: &mut GameClient<S>,
472    keys: &ClientKeyBindings,
473    action: InputAction,
474    ctx: &mut ActionCtx<'_>,
475) {
476    match action {
477        InputAction::Harvest => {
478            if let Err(err) = client.harvest_nearest().await {
479                client.state.push_log(format!("Harvest failed: {err}"));
480            }
481        }
482        InputAction::Pickup => {
483            if client.pickup_nearest().await.is_err() {
484                if let Err(err) = client.pickup_nearest_container().await {
485                    client.state.push_log(format!("Pickup: {err}"));
486                }
487            }
488        }
489        InputAction::Craft => client.open_craft_menu(),
490        InputAction::Interact | InputAction::UseWorld => {
491            if let Err(err) = client.use_nearest().await {
492                client.state.push_log(format!("Use: {err}"));
493            }
494        }
495        InputAction::TestDamage => {
496            if let Err(err) = client.test_damage(25.0).await {
497                client.state.push_log(format!("Damage failed: {err}"));
498            }
499        }
500        InputAction::CycleCombatTarget { reverse } => {
501            if let Err(err) = client.cycle_combat_target(reverse).await {
502                client.state.push_log(format!("T1 target: {err}"));
503            }
504        }
505        InputAction::CycleCombatTargetT2 { reverse } => {
506            if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
507                client.state.push_log(format!("T2 target: {err}"));
508            }
509        }
510        InputAction::AdvanceRotationT1 => {
511            if let Err(err) = client.advance_rotation(1).await {
512                client.state.push_log(format!("T1 step: {err}"));
513            }
514        }
515        InputAction::AdvanceRotationT2 => {
516            if let Err(err) = client.advance_rotation(2).await {
517                client.state.push_log(format!("T2 step: {err}"));
518            }
519        }
520        InputAction::ToggleAutoT1 => {
521            if let Err(err) = client.toggle_auto_attack_slot(1).await {
522                client.state.push_log(format!("T1 auto: {err}"));
523            }
524        }
525        InputAction::ToggleAutoT2 => {
526            if let Err(err) = client.toggle_auto_attack_slot(2).await {
527                client.state.push_log(format!("T2 auto: {err}"));
528            }
529        }
530        InputAction::ToggleLoadout => {
531            client.state.show_rotation_editor = false;
532            client.state.rotation_editor.reset();
533            client.state.show_loadout_menu = !client.state.show_loadout_menu;
534            if client.state.show_loadout_menu {
535                *ctx.auto_nav = None;
536                let _ = client.stop().await;
537            }
538        }
539        InputAction::ToggleRotationEditor => {
540            client.state.show_loadout_menu = false;
541            if client.state.show_rotation_editor {
542                client.state.show_rotation_editor = false;
543                client.state.rotation_editor.reset();
544            } else {
545                client.state.show_rotation_editor = true;
546                client.state.rotation_editor.reset();
547                let max = client.state.rotation_presets.len();
548                if max > 0 {
549                    client.state.rotation_editor.list_index =
550                        client.state.rotation_editor.list_index.min(max - 1);
551                }
552            }
553            if client.state.show_rotation_editor {
554                *ctx.auto_nav = None;
555                let _ = client.stop().await;
556            }
557        }
558        InputAction::LoadoutMenuUp => {
559            if client.state.show_loadout_menu && client.state.loadout_menu_index > 0 {
560                client.state.loadout_menu_index -= 1;
561            }
562        }
563        InputAction::LoadoutMenuDown => {
564            if client.state.show_loadout_menu {
565                let max = client.state.rotation_presets.len();
566                if max > 0 {
567                    client.state.loadout_menu_index =
568                        (client.state.loadout_menu_index + 1).min(max - 1);
569                }
570            }
571        }
572        InputAction::LoadoutAssignT1 => {
573            let preset_id = {
574                let idx = client.state.loadout_menu_index;
575                client.state.rotation_presets.get(idx).map(|p| p.id.clone())
576            };
577            if let Some(preset_id) = preset_id {
578                if let Err(err) = client.assign_slot_preset(1, &preset_id).await {
579                    client.state.push_log(format!("Loadout: {err}"));
580                }
581            }
582        }
583        InputAction::LoadoutAssignT2 => {
584            let preset_id = {
585                let idx = client.state.loadout_menu_index;
586                client.state.rotation_presets.get(idx).map(|p| p.id.clone())
587            };
588            if let Some(preset_id) = preset_id {
589                if let Err(err) = client.assign_slot_preset(2, &preset_id).await {
590                    client.state.push_log(format!("Loadout: {err}"));
591                }
592            }
593        }
594        InputAction::CloseOverlay => {
595            client.state.show_loadout_menu = false;
596            client.state.show_rotation_editor = false;
597            client.state.rotation_editor.reset();
598        }
599        InputAction::ClearCombatTarget => {
600            if let Err(err) = client.clear_combat_target().await {
601                client.state.push_log(format!("Target: {err}"));
602            }
603        }
604        InputAction::ClearCombatTargetT2 => {
605            if let Err(err) = client.clear_combat_target_slot(2).await {
606                client.state.push_log(format!("T2 target: {err}"));
607            }
608        }
609        InputAction::Dodge => {
610            *ctx.auto_nav = None;
611            if let Err(err) = client.dodge().await {
612                client.state.push_log(format!("Dodge: {err}"));
613            }
614        }
615        InputAction::Lunge => {
616            *ctx.auto_nav = None;
617            let _ = client.stop().await;
618            if let Err(err) = client.lunge().await {
619                client.state.push_log(format!("Lunge: {err}"));
620            }
621        }
622        InputAction::DirectionalJump { forward, strafe } => {
623            *ctx.auto_nav = None;
624            let _ = client.stop().await;
625            if let Err(err) = client.directional_jump(forward, strafe).await {
626                client.state.push_log(format!("Jump: {err}"));
627            }
628        }
629        InputAction::CastHotbar { slot } => match keys.hotbar_ability(slot) {
630            Some(ability_id) => {
631                let ability_id = ability_id.to_string();
632                if let Err(err) = client.cast_hotbar_ability(&ability_id).await {
633                    client.state.push_log(format!("Hotbar {slot}: {err}"));
634                }
635            }
636            None => {
637                client.state.push_log(format!(
638                    "Hotbar {slot} unbound — set in client-settings.yaml"
639                ));
640            }
641        },
642        InputAction::ToggleBlock => {
643            *ctx.block_active = !*ctx.block_active;
644            if let Err(err) = client.set_block(*ctx.block_active).await {
645                client.state.push_log(format!("Block: {err}"));
646                *ctx.block_active = false;
647            }
648        }
649        InputAction::ToggleStats => client.toggle_stats(),
650        InputAction::ToggleInventory => client.toggle_inventory_menu(),
651        InputAction::ToggleKeychain => client.toggle_keychain_menu(),
652        InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
653        InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
654        InputAction::QuestMenuUp => client.quest_menu_move(-1),
655        InputAction::QuestMenuDown => client.quest_menu_move(1),
656        InputAction::QuestWithdraw => client.quest_request_withdraw(),
657        InputAction::RotationEditorBack => {
658            let _ = client.back_on_esc();
659        }
660        InputAction::RotationEditorListUp
661        | InputAction::RotationEditorListDown
662        | InputAction::RotationEditorEdit
663        | InputAction::RotationEditorNew
664        | InputAction::RotationEditorDelete
665        | InputAction::RotationEditorAddAbility
666        | InputAction::RotationEditorRemoveAbility
667        | InputAction::RotationEditorMoveAbilityUp
668        | InputAction::RotationEditorMoveAbilityDown
669        | InputAction::RotationEditorAbilityUp
670        | InputAction::RotationEditorAbilityDown
671        | InputAction::RotationEditorPickerUp
672        | InputAction::RotationEditorPickerDown
673        | InputAction::RotationEditorPickAbility
674        | InputAction::RotationEditorRename
675        | InputAction::RotationEditorConfirmLabel
676        | InputAction::RotationEditorLabelBackspace
677        | InputAction::RotationEditorLabelChar(_)
678        | InputAction::RotationEditorSave => {
679            handle_rotation_editor_action(client, action).await;
680        }
681        InputAction::None
682        | InputAction::Quit
683        | InputAction::ToggleHelp
684        | InputAction::CycleHudView
685        | InputAction::StartChat { .. }
686        | InputAction::SubmitChat
687        | InputAction::CancelChat
688        | InputAction::ToggleSprintMode
689        | InputAction::ToggleMapTarget
690        | InputAction::ConfirmMapTarget
691        | InputAction::CancelMapTarget
692        | InputAction::MapTargetNudge { .. }
693        | InputAction::CancelAutoNav
694        | InputAction::StopMovement => {}
695    }
696}
697
698async fn handle_rotation_editor_action<S: PlayConnection>(
699    client: &mut GameClient<S>,
700    action: InputAction,
701) {
702    match action {
703        InputAction::RotationEditorListUp => {
704            if client.state.rotation_editor.list_index > 0 {
705                client.state.rotation_editor.list_index -= 1;
706            }
707        }
708        InputAction::RotationEditorListDown => {
709            let max = client.state.rotation_presets.len();
710            if max > 0 {
711                client.state.rotation_editor.list_index =
712                    (client.state.rotation_editor.list_index + 1).min(max - 1);
713            }
714        }
715        InputAction::RotationEditorEdit => {
716            let idx = client.state.rotation_editor.list_index;
717            if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
718                client.state.rotation_editor.draft = Some(preset);
719                client.state.rotation_editor.ability_index = 0;
720                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
721            }
722        }
723        InputAction::RotationEditorNew => {
724            let preset = next_custom_preset(&client.state.rotation_presets);
725            client.state.rotation_editor.list_index = client.state.rotation_presets.len();
726            client.state.rotation_editor.draft = Some(preset);
727            client.state.rotation_editor.ability_index = 0;
728            client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
729        }
730        InputAction::RotationEditorDelete => {
731            let idx = client.state.rotation_editor.list_index;
732            let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
733            if let Some(id) = preset_id {
734                if preset_deletable(&id) {
735                    if let Err(err) = client.delete_rotation_preset(&id).await {
736                        client.state.push_log(format!("Delete: {err}"));
737                    } else {
738                        let max = client.state.rotation_presets.len();
739                        client.state.rotation_editor.list_index = if max == 0 {
740                            0
741                        } else {
742                            client.state.rotation_editor.list_index.min(max - 1)
743                        };
744                    }
745                } else {
746                    client.state.push_log("Cannot delete built-in preset");
747                }
748            }
749        }
750        InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
751            RotationEditorMode::EditLabel => {
752                client.state.rotation_editor.label_buffer.clear();
753                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
754            }
755            RotationEditorMode::PickAbility => {
756                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
757            }
758            RotationEditorMode::EditSequence => {
759                client.state.rotation_editor.draft = None;
760                client.state.rotation_editor.mode = RotationEditorMode::List;
761            }
762            RotationEditorMode::List => {}
763        },
764        InputAction::RotationEditorAddAbility => {
765            client.state.rotation_editor.picker_index = 0;
766            client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
767        }
768        InputAction::RotationEditorRemoveAbility => {
769            let idx = client.state.rotation_editor.ability_index;
770            if let Some(draft) = &mut client.state.rotation_editor.draft {
771                if idx < draft.abilities.len() {
772                    draft.abilities.remove(idx);
773                    if client.state.rotation_editor.ability_index >= draft.abilities.len()
774                        && client.state.rotation_editor.ability_index > 0
775                    {
776                        client.state.rotation_editor.ability_index -= 1;
777                    }
778                }
779            }
780        }
781        InputAction::RotationEditorMoveAbilityUp => {
782            let i = client.state.rotation_editor.ability_index;
783            if let Some(draft) = &mut client.state.rotation_editor.draft {
784                if i > 0 && i < draft.abilities.len() {
785                    draft.abilities.swap(i, i - 1);
786                    client.state.rotation_editor.ability_index -= 1;
787                }
788            }
789        }
790        InputAction::RotationEditorMoveAbilityDown => {
791            let i = client.state.rotation_editor.ability_index;
792            if let Some(draft) = &mut client.state.rotation_editor.draft {
793                if i + 1 < draft.abilities.len() {
794                    draft.abilities.swap(i, i + 1);
795                    client.state.rotation_editor.ability_index += 1;
796                }
797            }
798        }
799        InputAction::RotationEditorAbilityUp => {
800            if client.state.rotation_editor.ability_index > 0 {
801                client.state.rotation_editor.ability_index -= 1;
802            }
803        }
804        InputAction::RotationEditorAbilityDown => {
805            if let Some(draft) = &client.state.rotation_editor.draft {
806                if !draft.abilities.is_empty() {
807                    client.state.rotation_editor.ability_index =
808                        (client.state.rotation_editor.ability_index + 1)
809                            .min(draft.abilities.len() - 1);
810                }
811            }
812        }
813        InputAction::RotationEditorPickerUp => {
814            if client.state.rotation_editor.picker_index > 0 {
815                client.state.rotation_editor.picker_index -= 1;
816            }
817        }
818        InputAction::RotationEditorPickerDown => {
819            if !EDITOR_ABILITIES.is_empty() {
820                client.state.rotation_editor.picker_index =
821                    (client.state.rotation_editor.picker_index + 1).min(EDITOR_ABILITIES.len() - 1);
822            }
823        }
824        InputAction::RotationEditorPickAbility => {
825            let pick = client.state.rotation_editor.picker_index;
826            if let Some(ability) = EDITOR_ABILITIES.get(pick) {
827                if let Some(draft) = &mut client.state.rotation_editor.draft {
828                    draft.abilities.push((*ability).to_string());
829                    client.state.rotation_editor.ability_index =
830                        draft.abilities.len().saturating_sub(1);
831                }
832                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
833            }
834        }
835        InputAction::RotationEditorRename => {
836            if let Some(draft) = &client.state.rotation_editor.draft {
837                client.state.rotation_editor.label_buffer = draft.label.clone();
838                client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
839            }
840        }
841        InputAction::RotationEditorConfirmLabel => {
842            let label = client.state.rotation_editor.label_buffer.trim().to_string();
843            if label.is_empty() {
844                client.state.push_log("Label cannot be empty");
845            } else if let Some(draft) = &mut client.state.rotation_editor.draft {
846                draft.label = label;
847                client.state.rotation_editor.label_buffer.clear();
848                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
849            }
850        }
851        InputAction::RotationEditorLabelBackspace => {
852            client.state.rotation_editor.label_buffer.pop();
853        }
854        InputAction::RotationEditorLabelChar(c) => {
855            if client.state.rotation_editor.label_buffer.len() < 32 {
856                client.state.rotation_editor.label_buffer.push(c);
857            }
858        }
859        InputAction::RotationEditorSave => {
860            let draft = match client.state.rotation_editor.draft.clone() {
861                Some(d) => d,
862                None => return,
863            };
864            if draft.abilities.is_empty() {
865                client.state.push_log("Rotation needs at least one ability");
866                return;
867            }
868            let saved_id = draft.id.clone();
869            if let Err(err) = client.upsert_rotation_preset(draft).await {
870                client.state.push_log(format!("Save: {err}"));
871            } else {
872                client.state.rotation_editor.mode = RotationEditorMode::List;
873                client.state.rotation_editor.draft = None;
874                if let Some(i) = client
875                    .state
876                    .rotation_presets
877                    .iter()
878                    .position(|p| p.id == saved_id)
879                {
880                    client.state.rotation_editor.list_index = i;
881                }
882            }
883        }
884        _ => {}
885    }
886}