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    editor_ability_choices, next_custom_preset, preset_deletable, InputAction, UiKeyCode,
8    UiKeyEvent, UiKeyEventKind,
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    // Chat typing owns the keyboard — no gameplay binds while focused.
26    if client.state.social_chat.input_focused {
27        match key.code {
28            UiKeyCode::Esc => client.state.social_chat.unfocus(),
29            UiKeyCode::Enter => {
30                if let Err(err) = client.submit_social_chat_buffer().await {
31                    client.state.push_log(format!("Chat failed: {err}"));
32                }
33            }
34            UiKeyCode::Tab => client.state.social_chat.toggle_mode_speak_whisper(),
35            UiKeyCode::Backspace => {
36                client.state.social_chat.buffer.pop();
37            }
38            UiKeyCode::Char(c) => {
39                // Accept printable text; ignore lone control leftovers.
40                if !c.is_control()
41                    && client.state.social_chat.buffer.len()
42                        < flatland_client_lib::SocialChatState::MAX_BUF
43                {
44                    client.state.social_chat.buffer.push(c);
45                }
46            }
47            _ => {}
48        }
49        return true;
50    }
51
52    if client.state.show_worker_rename {
53        match key.code {
54            UiKeyCode::Esc => client.cancel_worker_rename(),
55            UiKeyCode::Enter => {
56                if let Err(err) = client.confirm_worker_rename().await {
57                    client.state.push_log(format!("Rename: {err}"));
58                }
59            }
60            UiKeyCode::Backspace => {
61                client.state.rename_buffer.pop();
62            }
63            UiKeyCode::Char(c) => {
64                if client.state.rename_buffer.chars().count() < 32 {
65                    client.state.rename_buffer.push(c);
66                }
67            }
68            _ => {}
69        }
70        return true;
71    }
72
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                let max = if client.state.rename_plot_id.is_some() {
86                    48
87                } else {
88                    32
89                };
90                if client.state.rename_buffer.chars().count() < max {
91                    client.state.rename_buffer.push(c);
92                }
93            }
94            _ => {}
95        }
96        return true;
97    }
98
99    if client.state.show_equip_menu {
100        match key.code {
101            UiKeyCode::Char('p') | UiKeyCode::Esc => {
102                client.toggle_equip_menu();
103            }
104            UiKeyCode::Up | UiKeyCode::Char('k') => {
105                if client.state.equip_menu_index > 0 {
106                    client.state.equip_menu_index -= 1;
107                }
108            }
109            UiKeyCode::Down | UiKeyCode::Char('j') => {
110                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
111                if n > 0 {
112                    client.state.equip_menu_index = (client.state.equip_menu_index + 1).min(n - 1);
113                }
114            }
115            UiKeyCode::PageUp => {
116                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
117                client.state.equip_menu_index =
118                    flatland_client_lib::page_list_index(client.state.equip_menu_index, -1, n);
119            }
120            UiKeyCode::PageDown => {
121                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
122                client.state.equip_menu_index =
123                    flatland_client_lib::page_list_index(client.state.equip_menu_index, 1, n);
124            }
125            UiKeyCode::Enter => {
126                if let Err(err) = client.activate_equip_selection().await {
127                    client.state.push_log(format!("Equip: {err}"));
128                }
129            }
130            _ => {}
131        }
132        return true;
133    }
134
135    if client.state.show_inventory_menu {
136        if client.state.show_rename_prompt {
137            // Handled above (works for inventory and on-plot rename).
138            return true;
139        } else if client.state.show_destroy_picker {
140            match key.code {
141                UiKeyCode::Esc => {
142                    if client.state.destroy_confirm_pending {
143                        client.cancel_destroy_confirm();
144                    } else {
145                        client.close_destroy_picker();
146                    }
147                }
148                UiKeyCode::Char('-') => {
149                    if !client.state.destroy_confirm_pending {
150                        client.destroy_picker_adjust_quantity(-1);
151                    }
152                }
153                UiKeyCode::Char('+') | UiKeyCode::Char('=') => {
154                    if !client.state.destroy_confirm_pending {
155                        client.destroy_picker_adjust_quantity(1);
156                    }
157                }
158                UiKeyCode::Char('a') => {
159                    if !client.state.destroy_confirm_pending {
160                        client.destroy_picker_set_quantity_max();
161                    }
162                }
163                UiKeyCode::Char('0') => {
164                    if !client.state.destroy_confirm_pending {
165                        client.destroy_picker_set_quantity_min();
166                    }
167                }
168                UiKeyCode::Enter => {
169                    if let Err(err) = client.activate_inventory_selection().await {
170                        client.state.push_log(format!("Destroy failed: {err}"));
171                    }
172                }
173                _ => {}
174            }
175        } else if client.state.show_grant_picker {
176            let filter_focused = client
177                .state
178                .grant_picker
179                .as_ref()
180                .map(|p| p.filter_focused)
181                .unwrap_or(false);
182            match key.code {
183                UiKeyCode::Esc => {
184                    if !client.clear_or_blur_inventory_filter() {
185                        client.close_grant_picker();
186                    }
187                }
188                UiKeyCode::PageUp => client.inventory_menu_page(-1),
189                UiKeyCode::PageDown => client.inventory_menu_page(1),
190                UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
191                UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
192                UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
193                UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
194                    client.inventory_menu_move(-1)
195                }
196                UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
197                    client.inventory_menu_move(1)
198                }
199                UiKeyCode::Enter if !filter_focused => {
200                    if let Err(err) = client.activate_inventory_selection().await {
201                        client.state.push_log(format!("Grant failed: {err}"));
202                    }
203                }
204                _ => {}
205            }
206        } else if client.state.show_move_picker {
207            let filter_focused = client
208                .state
209                .move_picker
210                .as_ref()
211                .map(|p| p.filter_focused)
212                .unwrap_or(false);
213            match key.code {
214                UiKeyCode::Esc => {
215                    if !client.clear_or_blur_inventory_filter() {
216                        client.close_move_picker();
217                    }
218                }
219                UiKeyCode::PageUp => client.inventory_menu_page(-1),
220                UiKeyCode::PageDown => client.inventory_menu_page(1),
221                UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
222                UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
223                UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
224                UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
225                    client.inventory_menu_move(-1)
226                }
227                UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
228                    client.inventory_menu_move(1)
229                }
230                UiKeyCode::Char('-') if !filter_focused => {
231                    client.move_picker_adjust_quantity(-1);
232                }
233                UiKeyCode::Char('+') | UiKeyCode::Char('=') if !filter_focused => {
234                    client.move_picker_adjust_quantity(1);
235                }
236                UiKeyCode::Char('a') if !filter_focused => client.move_picker_set_quantity_max(),
237                UiKeyCode::Char('0') if !filter_focused => client.move_picker_set_quantity_min(),
238                UiKeyCode::Enter if !filter_focused => {
239                    if let Err(err) = client.activate_inventory_selection().await {
240                        client.state.push_log(format!("Move failed: {err}"));
241                    }
242                }
243                _ => {}
244            }
245        } else if client.state.inventory_filter_focused {
246            match key.code {
247                UiKeyCode::Esc => {
248                    let _ = client.clear_or_blur_inventory_filter();
249                }
250                UiKeyCode::Enter => {
251                    client.state.inventory_filter_focused = false;
252                }
253                UiKeyCode::Backspace => client.inventory_filter_backspace(),
254                UiKeyCode::Char(c) => client.append_inventory_filter_char(c),
255                _ => {}
256            }
257        } else {
258            match key.code {
259                UiKeyCode::Esc => {
260                    if !client.clear_or_blur_inventory_filter() {
261                        client.close_inventory_menu();
262                    }
263                }
264                UiKeyCode::Char('b') => client.close_inventory_menu(),
265                UiKeyCode::Char('p') => client.toggle_equip_menu(),
266                UiKeyCode::Tab => client.cycle_inventory_tab(true),
267                UiKeyCode::BackTab => client.cycle_inventory_tab(false),
268                UiKeyCode::PageUp => client.inventory_menu_page(-1),
269                UiKeyCode::PageDown => client.inventory_menu_page(1),
270                UiKeyCode::Char('/') => client.focus_inventory_filter(),
271                UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
272                UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
273                UiKeyCode::Enter => {
274                    if let Err(err) = client.activate_inventory_selection().await {
275                        client.state.push_log(format!("{err}"));
276                    }
277                }
278                UiKeyCode::Char('m') => {
279                    if let Err(err) = client.open_move_picker() {
280                        client.state.push_log(format!("{err}"));
281                    }
282                }
283                UiKeyCode::Char('e') => {
284                    if let Err(err) = client.use_selected_consumable().await {
285                        client.state.push_log(format!("Use: {err}"));
286                    }
287                }
288                UiKeyCode::Char('n') => {
289                    if let Err(err) = client.open_rename_prompt() {
290                        client.state.push_log(format!("{err}"));
291                    }
292                }
293                UiKeyCode::Char('d') => {
294                    if let Err(err) = client.drop_selected().await {
295                        client.state.push_log(format!("Drop: {err}"));
296                    }
297                }
298                UiKeyCode::Char('x') => {
299                    if let Err(err) = client.open_destroy_picker() {
300                        client.state.push_log(format!("Destroy: {err}"));
301                    }
302                }
303                UiKeyCode::Char('l') => {
304                    if let Err(err) = client.toggle_chest_lock_for_selection().await {
305                        client.state.push_log(format!("Lock: {err}"));
306                    }
307                }
308                UiKeyCode::Char('g') => {
309                    if let Err(err) = client.give_selected_inventory_to_worker().await {
310                        client.state.push_log(format!("Give: {err}"));
311                    }
312                }
313                UiKeyCode::Char('u') => {
314                    if let Err(err) = client.unequip_mainhand().await {
315                        client.state.push_log(format!("Unequip: {err}"));
316                    }
317                }
318                _ => {}
319            }
320        }
321        return true;
322    }
323
324    if client.state.show_keychain_menu {
325        match key.code {
326            UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
327            UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
328                client.keychain_menu_move(-1);
329            }
330            UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
331                client.keychain_menu_move(1);
332            }
333            UiKeyCode::PageUp => client.keychain_menu_page(-1),
334            UiKeyCode::PageDown => client.keychain_menu_page(1),
335            UiKeyCode::Enter => {
336                if let Err(err) = client.activate_keychain_selection().await {
337                    client.state.push_log(format!("Keychain: {err}"));
338                }
339            }
340            _ => {}
341        }
342        return true;
343    }
344
345    if client.state.show_craft_menu {
346        match key.code {
347            UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
348                client.close_craft_menu()
349            }
350            UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
351            UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
352            UiKeyCode::PageUp => client.craft_menu_page(-1),
353            UiKeyCode::PageDown => client.craft_menu_page(1),
354            UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
355            UiKeyCode::Char('+') | UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
356            UiKeyCode::Char('a') => client.craft_batch_set_max(),
357            UiKeyCode::Char('0') => client.craft_batch_set_min(),
358            UiKeyCode::Enter => {
359                if let Err(err) = client.craft_menu_selection().await {
360                    client.state.push_log(format!("Craft failed: {err}"));
361                }
362            }
363            _ => {}
364        }
365        return true;
366    }
367
368    if client.state.show_plot_build_menu {
369        let building = client
370            .state
371            .timed_channel
372            .as_ref()
373            .is_some_and(|c| c.channel == flatland_protocol::TimedChannelKind::Build);
374        match key.code {
375            UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('B') => {
376                client.close_plot_build_menu()
377            }
378            UiKeyCode::Char('x') | UiKeyCode::Char('X') if building => {
379                if let Err(err) = client.plot_build_menu_cancel_build().await {
380                    client.state.push_log(format!("Build: {err}"));
381                }
382            }
383            UiKeyCode::Up | UiKeyCode::Char('k') if !building => client.plot_build_menu_move(-1),
384            UiKeyCode::Down | UiKeyCode::Char('j') if !building => client.plot_build_menu_move(1),
385            UiKeyCode::Tab if !building => client.plot_build_menu_toggle_focus(),
386            UiKeyCode::Enter if !building => {
387                if let Err(err) = client.plot_build_menu_confirm().await {
388                    client.state.push_log(format!("Build: {err}"));
389                }
390            }
391            _ => {}
392        }
393        return true;
394    }
395
396    if client.state.show_quest_offer {
397        match key.code {
398            UiKeyCode::Esc => client.quest_offer_decline(),
399            UiKeyCode::Enter => {
400                if let Err(err) = client.quest_offer_accept().await {
401                    client.state.push_log(format!("Quest: {err}"));
402                }
403            }
404            _ => {}
405        }
406        return true;
407    }
408
409    if client.state.show_npc_chat {
410        match key.code {
411            UiKeyCode::Esc => {
412                if let Err(err) = client.npc_talk_close().await {
413                    client.state.push_log(format!("Talk close failed: {err}"));
414                }
415            }
416            UiKeyCode::Enter => {
417                if let Err(err) = client.npc_talk_send().await {
418                    client.state.push_log(format!("Talk failed: {err}"));
419                }
420            }
421            UiKeyCode::Char(c @ '1'..='9') => {
422                let idx = (c as u8 - b'1') as usize;
423                if let Err(err) = client.npc_talk_topic(idx).await {
424                    client.state.push_log(format!("Talk failed: {err}"));
425                }
426            }
427            UiKeyCode::Backspace => {
428                if let Some(chat) = client.state.npc_chat.as_mut() {
429                    chat.input.pop();
430                }
431            }
432            UiKeyCode::Char(c) => {
433                if let Some(chat) = client.state.npc_chat.as_mut() {
434                    if !chat.pending && chat.input.len() < 300 {
435                        chat.input.push(c);
436                    }
437                }
438            }
439            _ => {}
440        }
441        return true;
442    }
443
444    if client.state.show_npc_verb_menu {
445        match key.code {
446            UiKeyCode::Esc => {
447                client.state.show_npc_verb_menu = false;
448                client.state.npc_verb_target = None;
449            }
450            UiKeyCode::Up | UiKeyCode::Char('k') => {
451                if client.state.npc_verb_index > 0 {
452                    client.state.npc_verb_index -= 1;
453                }
454            }
455            UiKeyCode::Down | UiKeyCode::Char('j') => {
456                let max = client.npc_verb_options().len().saturating_sub(1);
457                if client.state.npc_verb_index < max {
458                    client.state.npc_verb_index += 1;
459                }
460            }
461            UiKeyCode::Enter => {
462                if let Err(err) = client.confirm_npc_verb().await {
463                    client.state.push_log(format!("Interact failed: {err}"));
464                }
465            }
466            _ => {}
467        }
468        return true;
469    }
470
471    // Trade request accept/decline — not while typing chat.
472    if client.state.social_chat.pending_trade.is_some()
473        && !client.state.social_chat.input_focused
474        && !client.state.social_chat.picking_stone
475        && matches!(key.code, UiKeyCode::Char('y' | 'Y' | 'n' | 'N'))
476    {
477        let accept = matches!(key.code, UiKeyCode::Char('y' | 'Y'));
478        if let Err(err) = client.respond_pending_trade(accept).await {
479            client
480                .state
481                .push_log(format!("Trade respond failed: {err}"));
482        }
483        return true;
484    }
485
486    if client.state.player_verbs.open {
487        let opts = flatland_client_lib::PlayerVerbState::options();
488        match key.code {
489            UiKeyCode::Esc => client.state.player_verbs.close(),
490            UiKeyCode::Up | UiKeyCode::Char('k') => {
491                if client.state.player_verbs.index > 0 {
492                    client.state.player_verbs.index -= 1;
493                }
494            }
495            UiKeyCode::Down | UiKeyCode::Char('j') => {
496                let max = opts.len().saturating_sub(1);
497                if client.state.player_verbs.index < max {
498                    client.state.player_verbs.index += 1;
499                }
500            }
501            UiKeyCode::Enter => {
502                if let Err(err) = client.confirm_player_verb().await {
503                    client.state.push_log(format!("Interact failed: {err}"));
504                }
505            }
506            _ => {}
507        }
508        return true;
509    }
510
511    // Whisper-stone contact picker lives in the CHAT column (`g`).
512    if client.state.social_chat.picking_stone {
513        let contacts =
514            flatland_client_lib::contacts_from_stacks(&client.state.whisper_pouch_stacks);
515        match key.code {
516            UiKeyCode::Esc => {
517                client.state.social_chat.picking_stone = false;
518            }
519            UiKeyCode::Up | UiKeyCode::Char('k') => {
520                if client.state.social_chat.stone_pick_index > 0 {
521                    client.state.social_chat.stone_pick_index -= 1;
522                }
523            }
524            UiKeyCode::Down | UiKeyCode::Char('j') => {
525                let max = contacts.len().saturating_sub(1);
526                if client.state.social_chat.stone_pick_index < max {
527                    client.state.social_chat.stone_pick_index += 1;
528                }
529            }
530            UiKeyCode::Enter => {
531                if let Some(c) = contacts.get(client.state.social_chat.stone_pick_index) {
532                    if !c.blank {
533                        if let Some(peer) = client
534                            .state
535                            .entities
536                            .iter()
537                            .find(|e| e.id != client.state.entity_id && e.label == c.peer_label)
538                        {
539                            client.state.social_chat.focus_stone(peer.id, &c.peer_label);
540                        } else {
541                            client.state.social_chat.push_system(format!(
542                                "{} is not online in this region",
543                                c.peer_label
544                            ));
545                            client.state.social_chat.picking_stone = false;
546                        }
547                    }
548                }
549            }
550            _ => {}
551        }
552        return true;
553    }
554
555    if client.state.trade_ui.panel.is_some() {
556        // Quantity entry for presenting a stack.
557        if client.state.trade_ui.qty_entry.is_some() {
558            match key.code {
559                UiKeyCode::Esc => {
560                    client.state.trade_ui.qty_entry = None;
561                    client.state.trade_ui.picking_inventory = true;
562                }
563                UiKeyCode::Enter => {
564                    if let Err(err) = client.trade_confirm_qty_or_present().await {
565                        client.state.push_log(format!("Present failed: {err}"));
566                    }
567                }
568                UiKeyCode::Char('-') => {
569                    client.state.trade_ui.adjust_qty(-1);
570                }
571                UiKeyCode::Char('+') | UiKeyCode::Char('=') => {
572                    client.state.trade_ui.adjust_qty(1);
573                }
574                UiKeyCode::Char('a') | UiKeyCode::Char('A') => {
575                    client.state.trade_ui.set_qty_all();
576                }
577                UiKeyCode::Char('0') => {
578                    let typed_empty = client
579                        .state
580                        .trade_ui
581                        .qty_entry
582                        .as_ref()
583                        .map(|e| e.typed.is_empty())
584                        .unwrap_or(true);
585                    if typed_empty {
586                        client.state.trade_ui.set_qty_min();
587                    } else {
588                        client.state.trade_ui.append_qty_digit('0');
589                    }
590                }
591                UiKeyCode::Backspace => client.state.trade_ui.qty_backspace(),
592                UiKeyCode::Char(c) if c.is_ascii_digit() => {
593                    client.state.trade_ui.append_qty_digit(c);
594                }
595                _ => {}
596            }
597            return true;
598        }
599        match key.code {
600            UiKeyCode::Esc => {
601                if client.state.trade_ui.picking_inventory {
602                    client.state.trade_ui.picking_inventory = false;
603                } else if let Err(err) = client.trade_cancel().await {
604                    client.state.push_log(format!("Trade cancel failed: {err}"));
605                }
606            }
607            UiKeyCode::Char('r') => {
608                let ready = client
609                    .state
610                    .trade_ui
611                    .panel
612                    .as_ref()
613                    .map(|p| !p.i_ready)
614                    .unwrap_or(true);
615                if let Err(err) = client.trade_set_ready(ready).await {
616                    client.state.push_log(format!("Trade ready failed: {err}"));
617                }
618            }
619            UiKeyCode::Char('p') => {
620                client.state.trade_ui.picking_inventory = !client.state.trade_ui.picking_inventory;
621                client.state.trade_ui.qty_entry = None;
622                if client.state.trade_ui.picking_inventory {
623                    let max = client
624                        .state
625                        .trade_presentable_stacks()
626                        .len()
627                        .saturating_sub(1);
628                    client.state.trade_ui.inventory_index =
629                        client.state.trade_ui.inventory_index.min(max);
630                }
631            }
632            UiKeyCode::Up | UiKeyCode::Char('k') => {
633                if client.state.trade_ui.picking_inventory {
634                    if client.state.trade_ui.inventory_index > 0 {
635                        client.state.trade_ui.inventory_index -= 1;
636                    }
637                } else if client.state.trade_ui.select_index > 0 {
638                    client.state.trade_ui.select_index -= 1;
639                }
640            }
641            UiKeyCode::Down | UiKeyCode::Char('j') => {
642                if client.state.trade_ui.picking_inventory {
643                    let max = client
644                        .state
645                        .trade_presentable_stacks()
646                        .len()
647                        .saturating_sub(1);
648                    if client.state.trade_ui.inventory_index < max {
649                        client.state.trade_ui.inventory_index += 1;
650                    }
651                }
652            }
653            UiKeyCode::PageUp => {
654                if client.state.trade_ui.picking_inventory {
655                    let n = client.state.trade_presentable_stacks().len();
656                    client.state.trade_ui.inventory_index = flatland_client_lib::page_list_index(
657                        client.state.trade_ui.inventory_index,
658                        -1,
659                        n,
660                    );
661                }
662            }
663            UiKeyCode::PageDown => {
664                if client.state.trade_ui.picking_inventory {
665                    let n = client.state.trade_presentable_stacks().len();
666                    client.state.trade_ui.inventory_index = flatland_client_lib::page_list_index(
667                        client.state.trade_ui.inventory_index,
668                        1,
669                        n,
670                    );
671                }
672            }
673            UiKeyCode::Enter => {
674                if client.state.trade_ui.picking_inventory {
675                    if let Err(err) = client.trade_confirm_qty_or_present().await {
676                        client.state.push_log(format!("Present failed: {err}"));
677                    }
678                }
679            }
680            _ => {}
681        }
682        return true;
683    }
684
685    if client.state.whisper_pouch_ui.open {
686        let contacts =
687            flatland_client_lib::contacts_from_stacks(&client.state.whisper_pouch_stacks);
688        match key.code {
689            UiKeyCode::Esc => client.state.whisper_pouch_ui.open = false,
690            UiKeyCode::Up | UiKeyCode::Char('k') => {
691                if client.state.whisper_pouch_ui.index > 0 {
692                    client.state.whisper_pouch_ui.index -= 1;
693                }
694            }
695            UiKeyCode::Down | UiKeyCode::Char('j') => {
696                let max = contacts.len().saturating_sub(1);
697                if client.state.whisper_pouch_ui.index < max {
698                    client.state.whisper_pouch_ui.index += 1;
699                }
700            }
701            UiKeyCode::Enter => {
702                if let Some(c) = contacts.get(client.state.whisper_pouch_ui.index) {
703                    if !c.blank {
704                        // Resolve online peer by label match for stone chat.
705                        if let Some(peer) = client
706                            .state
707                            .entities
708                            .iter()
709                            .find(|e| e.id != client.state.entity_id && e.label == c.peer_label)
710                        {
711                            client.state.social_chat.open_stone(peer.id, &c.peer_label);
712                            client.state.whisper_pouch_ui.open = false;
713                        } else {
714                            client
715                                .state
716                                .push_log(format!("{} is not online in this region", c.peer_label));
717                        }
718                    }
719                }
720            }
721            UiKeyCode::Char('x') | UiKeyCode::Char('d') => {
722                if let Some(c) = contacts.get(client.state.whisper_pouch_ui.index) {
723                    let id = c.instance_id;
724                    if let Err(err) = client.destroy_whisper_stone(id).await {
725                        client
726                            .state
727                            .push_log(format!("Destroy stone failed: {err}"));
728                    }
729                }
730            }
731            _ => {}
732        }
733        return true;
734    }
735
736    if client.state.bank_panel.is_some() {
737        match &client.state.bank_ui_mode {
738            flatland_client_lib::BankUiMode::Menu => match key.code {
739                UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
740                    if let Err(err) = client.close_bank_panel().await {
741                        client.state.push_log(format!("Bank close failed: {err}"));
742                    }
743                }
744                UiKeyCode::Up | UiKeyCode::Char('k') => client.bank_menu_move(-1),
745                UiKeyCode::Down | UiKeyCode::Char('j') => client.bank_menu_move(1),
746                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
747                    if let Err(err) = client.confirm_bank_menu().await {
748                        client.state.push_log(format!("Bank action failed: {err}"));
749                    }
750                }
751                UiKeyCode::Char('d') => {
752                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::DepositAmount {
753                        input: String::new(),
754                    };
755                }
756                UiKeyCode::Char('w') => {
757                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::WithdrawAmount {
758                        input: String::new(),
759                    };
760                }
761                UiKeyCode::Char('t') => {
762                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::TransferName {
763                        input: String::new(),
764                    };
765                }
766                _ => {}
767            },
768            _ => match key.code {
769                UiKeyCode::Esc | UiKeyCode::Char('[') => client.bank_transfer_back(),
770                UiKeyCode::Enter => {
771                    if let Err(err) = client.confirm_bank_menu().await {
772                        client.state.push_log(format!("Bank action failed: {err}"));
773                    }
774                }
775                UiKeyCode::Backspace => client.bank_transfer_backspace(),
776                UiKeyCode::Char(c) => client.bank_transfer_append_char(c),
777                _ => {}
778            },
779        }
780        return true;
781    }
782
783    if client.state.storage_panel.is_some() {
784        match &client.state.storage_ui_mode {
785            flatland_client_lib::StorageUiMode::Menu => match key.code {
786                UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
787                    if let Err(err) = client.close_storage_panel().await {
788                        client
789                            .state
790                            .push_log(format!("Storage close failed: {err}"));
791                    }
792                }
793                UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_menu_move(-1),
794                UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_menu_move(1),
795                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
796                    if let Err(err) = client.confirm_storage_menu().await {
797                        client
798                            .state
799                            .push_log(format!("Storage action failed: {err}"));
800                    }
801                }
802                UiKeyCode::Char('s') => {
803                    let opts = client.state.storage_store_options();
804                    if opts.is_empty() {
805                        client.state.push_log("Nothing loose to store.");
806                    } else {
807                        client.state.storage_ui_mode =
808                            flatland_client_lib::StorageUiMode::StorePick { index: 0 };
809                    }
810                }
811                UiKeyCode::Char('t') => {
812                    let opts = client.state.storage_vault_options();
813                    if opts.is_empty() {
814                        client.state.push_log("Vault is empty.");
815                    } else {
816                        client.state.storage_ui_mode =
817                            flatland_client_lib::StorageUiMode::TakePick { index: 0 };
818                    }
819                }
820                _ => {}
821            },
822            flatland_client_lib::StorageUiMode::StoreAmount { .. }
823            | flatland_client_lib::StorageUiMode::TakeAmount { .. }
824            | flatland_client_lib::StorageUiMode::ShipAmount { .. } => match key.code {
825                UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
826                UiKeyCode::Enter => {
827                    if let Err(err) = client.confirm_storage_menu().await {
828                        client
829                            .state
830                            .push_log(format!("Storage action failed: {err}"));
831                    }
832                }
833                UiKeyCode::Backspace => client.storage_amount_backspace(),
834                UiKeyCode::Char(c) => client.storage_amount_append_char(c),
835                _ => {}
836            },
837            _ => match key.code {
838                UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
839                UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_pick_move(-1),
840                UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_pick_move(1),
841                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
842                    if let Err(err) = client.confirm_storage_menu().await {
843                        client
844                            .state
845                            .push_log(format!("Storage action failed: {err}"));
846                    }
847                }
848                _ => {}
849            },
850        }
851        return true;
852    }
853
854    if client.state.market_panel.is_some() {
855        let filter_focused = client.state.market_filter_focused;
856        match &client.state.market_ui_mode {
857            flatland_client_lib::MarketUiMode::Browse => {
858                if filter_focused {
859                    match key.code {
860                        UiKeyCode::Esc => {
861                            let _ = client.clear_or_blur_market_filter();
862                        }
863                        UiKeyCode::Enter => {
864                            client.state.market_filter_focused = false;
865                        }
866                        UiKeyCode::Backspace => client.market_filter_backspace(),
867                        UiKeyCode::Char(c) => client.append_market_filter_char(c),
868                        _ => {}
869                    }
870                    return true;
871                }
872                match key.code {
873                    UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
874                        if client.state.market_buy_confirm.is_some() {
875                            client.state.market_buy_confirm = None;
876                        } else if client.clear_or_blur_market_filter() {
877                            // cleared search / category stays
878                        } else if let Err(err) = client.close_market_panel().await {
879                            client.state.push_log(format!("Market close failed: {err}"));
880                        }
881                    }
882                    UiKeyCode::Up | UiKeyCode::Char('k') => {
883                        if client.state.market_buy_confirm.is_none() {
884                            client.market_move_selection(-1);
885                        }
886                    }
887                    UiKeyCode::Down | UiKeyCode::Char('j') => {
888                        if client.state.market_buy_confirm.is_none() {
889                            client.market_move_selection(1);
890                        }
891                    }
892                    UiKeyCode::PageUp => {
893                        if client.state.market_buy_confirm.is_none() {
894                            client.market_page_selection(-1);
895                        }
896                    }
897                    UiKeyCode::PageDown => {
898                        if client.state.market_buy_confirm.is_none() {
899                            client.market_page_selection(1);
900                        }
901                    }
902                    UiKeyCode::Tab => {
903                        if client.state.market_buy_confirm.is_none() {
904                            client.market_cycle_category(1);
905                        }
906                    }
907                    UiKeyCode::Char('/') => client.focus_market_filter(),
908                    UiKeyCode::Enter | UiKeyCode::Char(' ') => {
909                        if let Err(err) = client.market_activate_selection().await {
910                            client
911                                .state
912                                .push_log(format!("Market action failed: {err}"));
913                        }
914                    }
915                    UiKeyCode::Char('l') => client.market_begin_list(),
916                    _ => {}
917                }
918            }
919            flatland_client_lib::MarketUiMode::ListAmount { .. }
920            | flatland_client_lib::MarketUiMode::ListPrice { .. } => match key.code {
921                UiKeyCode::Esc | UiKeyCode::Char('[') => client.market_ui_back(),
922                UiKeyCode::Enter => {
923                    if let Err(err) = client.confirm_market_list_step().await {
924                        client.state.push_log(format!("Market list failed: {err}"));
925                    }
926                }
927                UiKeyCode::Backspace => client.market_list_amount_backspace(),
928                UiKeyCode::Char(c) => client.market_list_amount_append_char(c),
929                _ => {}
930            },
931            _ => {
932                if filter_focused {
933                    match key.code {
934                        UiKeyCode::Esc => {
935                            let _ = client.clear_or_blur_market_filter();
936                        }
937                        UiKeyCode::Enter => {
938                            client.state.market_filter_focused = false;
939                        }
940                        UiKeyCode::Backspace => client.market_filter_backspace(),
941                        UiKeyCode::Char(c) => client.append_market_filter_char(c),
942                        _ => {}
943                    }
944                    return true;
945                }
946                match key.code {
947                    UiKeyCode::Esc | UiKeyCode::Char('[') => {
948                        if !client.clear_or_blur_market_filter() {
949                            client.market_ui_back();
950                        }
951                    }
952                    UiKeyCode::Up | UiKeyCode::Char('k') => client.market_list_move(-1),
953                    UiKeyCode::Down | UiKeyCode::Char('j') => client.market_list_move(1),
954                    UiKeyCode::PageUp => client.market_list_page(-1),
955                    UiKeyCode::PageDown => client.market_list_page(1),
956                    UiKeyCode::Tab => client.market_cycle_category(1),
957                    UiKeyCode::Char('/') => client.focus_market_filter(),
958                    UiKeyCode::Enter | UiKeyCode::Char(' ') => {
959                        if let Err(err) = client.confirm_market_list_step().await {
960                            client.state.push_log(format!("Market list failed: {err}"));
961                        }
962                    }
963                    _ => {}
964                }
965            }
966        }
967        return true;
968    }
969
970    if client.state.show_shop_menu {
971        match key.code {
972            UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
973                if let Err(err) = client.back_from_shop_menu().await {
974                    client.state.push_log(format!("Shop close failed: {err}"));
975                }
976            }
977            UiKeyCode::Tab => client.shop_tab_toggle(),
978            UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
979            UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
980            UiKeyCode::PageUp => client.shop_menu_page(-1),
981            UiKeyCode::PageDown => client.shop_menu_page(1),
982            UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
983            UiKeyCode::Char('+') | UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
984            UiKeyCode::Char('a') => client.shop_quantity_set_max(),
985            UiKeyCode::Char('0') => client.shop_quantity_set_min(),
986            UiKeyCode::Enter => {
987                if let Err(err) = client.shop_confirm().await {
988                    client.state.push_log(format!("Trade failed: {err}"));
989                }
990            }
991            _ => {}
992        }
993        return true;
994    }
995
996    if client.state.show_quest_menu {
997        match key.code {
998            UiKeyCode::Esc => {
999                if client.state.quest_withdraw_confirm {
1000                    client.state.quest_withdraw_confirm = false;
1001                } else {
1002                    client.state.show_quest_menu = false;
1003                }
1004            }
1005            UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
1006            UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
1007            UiKeyCode::PageUp => client.quest_menu_page(-1),
1008            UiKeyCode::PageDown => client.quest_menu_page(1),
1009            UiKeyCode::Char('x') => client.quest_request_withdraw(),
1010            UiKeyCode::Enter => {
1011                if let Err(err) = client.quest_confirm_action().await {
1012                    client.state.push_log(format!("Quest: {err}"));
1013                }
1014            }
1015            _ => {}
1016        }
1017        return true;
1018    }
1019
1020    if client.state.worker_route_editor.is_some() {
1021        let at_root = client.re_at_root_sheet();
1022        if at_root {
1023            // Root: the ordered stop list.
1024            match key.code {
1025                UiKeyCode::Esc => client.close_worker_route_editor(),
1026                UiKeyCode::Char('s') => {
1027                    if let Err(err) = client.worker_route_editor_save().await {
1028                        client.state.push_log(format!("Route: {err}"));
1029                    }
1030                }
1031                UiKeyCode::Down => {
1032                    if key.modifiers.contains_control() {
1033                        client.worker_route_editor_move_selected(1);
1034                    } else {
1035                        client.worker_route_editor_select(1);
1036                    }
1037                }
1038                UiKeyCode::Up => {
1039                    if key.modifiers.contains_control() {
1040                        client.worker_route_editor_move_selected(-1);
1041                    } else {
1042                        client.worker_route_editor_select(-1);
1043                    }
1044                }
1045                // Select: vim j/k. Reorder: Ctrl+j = earlier (up), Ctrl+k = later (down)
1046                // — matches "k moves down" and keeps arrow+Ctrl natural.
1047                UiKeyCode::Char('j') => {
1048                    if key.modifiers.contains_control() {
1049                        client.worker_route_editor_move_selected(-1);
1050                    } else {
1051                        client.worker_route_editor_select(1);
1052                    }
1053                }
1054                UiKeyCode::Char('k') => {
1055                    if key.modifiers.contains_control() {
1056                        client.worker_route_editor_move_selected(1);
1057                    } else {
1058                        client.worker_route_editor_select(-1);
1059                    }
1060                }
1061                UiKeyCode::Char('d') | UiKeyCode::Delete => {
1062                    client.worker_route_editor_delete_selected()
1063                }
1064                UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
1065                UiKeyCode::Char('a') => client.re_open_add_menu(),
1066                UiKeyCode::Char('l') => client.re_open_bed_picker(),
1067                UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
1068                UiKeyCode::Enter => client.re_edit_selected_stop(),
1069                _ => {}
1070            }
1071        } else {
1072            // Inside a setup sheet: uniform picker keys.
1073            let filter_focused = client
1074                .state
1075                .worker_route_editor
1076                .as_ref()
1077                .map(|ed| ed.sheet_filter_focused)
1078                .unwrap_or(false);
1079            let sheet_index = client.re_sheet_index();
1080
1081            if filter_focused {
1082                // Search mode: every key is filter text until Enter (no j/k/list binds).
1083                match key.code {
1084                    UiKeyCode::Esc => {
1085                        let _ = client.clear_or_blur_re_sheet_filter();
1086                    }
1087                    UiKeyCode::Enter => client.re_blur_sheet_filter_keep_text(),
1088                    UiKeyCode::Backspace => client.re_sheet_filter_backspace(),
1089                    UiKeyCode::Char(c) => client.re_append_sheet_filter_char(c),
1090                    _ => {}
1091                }
1092            } else {
1093                match key.code {
1094                    UiKeyCode::Esc => {
1095                        if !client.clear_or_blur_re_sheet_filter() {
1096                            client.re_sheet_back();
1097                        }
1098                    }
1099                    UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
1100                    UiKeyCode::PageUp => client.re_sheet_page(-1),
1101                    UiKeyCode::PageDown => client.re_sheet_page(1),
1102                    UiKeyCode::Char('/') => client.re_focus_sheet_filter(),
1103                    UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
1104                    UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
1105                    UiKeyCode::Enter | UiKeyCode::Char(' ') => {
1106                        client.re_sheet_row_activate(sheet_index)
1107                    }
1108                    UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
1109                    UiKeyCode::Char('+') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
1110                    _ => {}
1111                }
1112            }
1113        }
1114        return true;
1115    }
1116
1117    if client.state.show_plant_menu {
1118        match key.code {
1119            UiKeyCode::Esc => client.close_plant_menu(),
1120            UiKeyCode::Up | UiKeyCode::Char('k') => client.plant_menu_move(-1),
1121            UiKeyCode::Down | UiKeyCode::Char('j') => client.plant_menu_move(1),
1122            UiKeyCode::Char('-') => client.plant_menu_adjust_quantity(-1),
1123            UiKeyCode::Char('+') | UiKeyCode::Char('=') => client.plant_menu_adjust_quantity(1),
1124            UiKeyCode::Char('a') | UiKeyCode::Char('A') => client.plant_menu_set_quantity_max(),
1125            UiKeyCode::Char('0') => client.plant_menu_set_quantity_min(),
1126            UiKeyCode::Enter | UiKeyCode::Char('f') | UiKeyCode::Char('F') => {
1127                if let Err(err) = client.confirm_plant_menu().await {
1128                    client.state.push_log(format!("Plant: {err}"));
1129                }
1130            }
1131            _ => {}
1132        }
1133        return true;
1134    }
1135
1136    if client.state.show_farm_access {
1137        match key.code {
1138            UiKeyCode::Esc => client.close_farm_access_panel(),
1139            UiKeyCode::Up | UiKeyCode::Char('k') => client.farm_access_move(-1),
1140            UiKeyCode::Down | UiKeyCode::Char('j') => client.farm_access_move(1),
1141            UiKeyCode::Enter | UiKeyCode::Char(' ') => {
1142                if let Err(err) = client.farm_access_activate().await {
1143                    client.state.push_log(format!("Farm access: {err}"));
1144                }
1145            }
1146            UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
1147                if let Err(err) = client.farm_access_adjust_discount(-100).await {
1148                    client.state.push_log(format!("Farm access: {err}"));
1149                }
1150            }
1151            UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
1152                if let Err(err) = client.farm_access_adjust_discount(100).await {
1153                    client.state.push_log(format!("Farm access: {err}"));
1154                }
1155            }
1156            _ => {}
1157        }
1158        return true;
1159    }
1160
1161    if client.state.worker_dismiss_confirmation.is_some() {
1162        match key.code {
1163            UiKeyCode::Esc => client.cancel_worker_dismissal(),
1164            UiKeyCode::Enter => {
1165                if let Err(err) = client.confirm_worker_dismissal().await {
1166                    client.state.push_log(format!("Worker: {err}"));
1167                }
1168            }
1169            _ => {}
1170        }
1171        return true;
1172    }
1173
1174    if client.state.show_worker_give_picker {
1175        match key.code {
1176            UiKeyCode::Esc => client.close_worker_give_picker(),
1177            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
1178            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
1179            UiKeyCode::Enter => {
1180                if let Err(err) = client.confirm_worker_give_picker().await {
1181                    client.state.push_log(format!("Give: {err}"));
1182                }
1183            }
1184            _ => {}
1185        }
1186        return true;
1187    }
1188
1189    if client.state.show_worker_give_target_picker {
1190        match key.code {
1191            UiKeyCode::Esc => client.close_worker_give_target_picker(),
1192            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_target_picker_move(-1),
1193            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_target_picker_move(1),
1194            UiKeyCode::Enter => {
1195                if let Err(err) = client.confirm_worker_give_target_picker().await {
1196                    client.state.push_log(format!("Give: {err}"));
1197                }
1198            }
1199            _ => {}
1200        }
1201        return true;
1202    }
1203
1204    if client.state.show_worker_take_picker {
1205        match key.code {
1206            UiKeyCode::Esc => client.close_worker_take_picker(),
1207            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_take_picker_move(-1),
1208            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_take_picker_move(1),
1209            UiKeyCode::Char('-') => client.worker_take_picker_adjust_quantity(-1),
1210            UiKeyCode::Char('+') | UiKeyCode::Char('=') => {
1211                client.worker_take_picker_adjust_quantity(1)
1212            }
1213            UiKeyCode::Char('a') => client.worker_take_picker_set_quantity_max(),
1214            UiKeyCode::Char('0') => client.worker_take_picker_set_quantity_min(),
1215            UiKeyCode::Enter => {
1216                if let Err(err) = client.confirm_worker_take_picker().await {
1217                    client.state.push_log(format!("Take: {err}"));
1218                }
1219            }
1220            _ => {}
1221        }
1222        return true;
1223    }
1224
1225    if client.state.show_worker_teach_picker {
1226        match key.code {
1227            UiKeyCode::Esc => client.close_worker_teach_picker(),
1228            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
1229            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
1230            UiKeyCode::Enter => {
1231                if let Err(err) = client.confirm_worker_teach_picker().await {
1232                    client.state.push_log(format!("Teach: {err}"));
1233                }
1234            }
1235            _ => {}
1236        }
1237        return true;
1238    }
1239
1240    if client.state.show_workers_menu {
1241        match key.code {
1242            UiKeyCode::Esc => {
1243                if let Err(err) = client.close_workers_menu().await {
1244                    client.state.push_log(format!("Worker: {err}"));
1245                }
1246            }
1247            UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
1248            UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
1249            UiKeyCode::PageUp => client.workers_menu_page(-1),
1250            UiKeyCode::PageDown => client.workers_menu_page(1),
1251            UiKeyCode::Char('d') => {
1252                if let Err(err) = client.request_worker_dismissal() {
1253                    client.state.push_log(format!("Worker: {err}"));
1254                }
1255            }
1256            UiKeyCode::Char('r') => {
1257                if let Err(err) = client.hire_worker_laborer().await {
1258                    client.state.push_log(format!("Hire: {err}"));
1259                }
1260            }
1261            UiKeyCode::Char('e') => {
1262                if let Err(err) = client.open_worker_route_editor_for_selected() {
1263                    client.state.push_log(format!("Route: {err}"));
1264                }
1265            }
1266            UiKeyCode::Char('n') => {
1267                if let Err(err) = client.open_worker_rename() {
1268                    client.state.push_log(format!("Rename: {err}"));
1269                }
1270            }
1271            UiKeyCode::Char('g') => {
1272                if let Err(err) = client.open_worker_give_picker() {
1273                    client.state.push_log(format!("Give: {err}"));
1274                }
1275            }
1276            UiKeyCode::Char('i') => {
1277                if let Err(err) = client.open_worker_take_picker() {
1278                    client.state.push_log(format!("Take: {err}"));
1279                }
1280            }
1281            UiKeyCode::Char('t') => {
1282                if let Err(err) = client.open_worker_teach_picker() {
1283                    client.state.push_log(format!("Teach: {err}"));
1284                }
1285            }
1286            UiKeyCode::Char('p') => {
1287                if let Err(err) = client.workers_deliver_selected_to_storage().await {
1288                    client.state.push_log(format!("Deliver: {err}"));
1289                }
1290            }
1291            UiKeyCode::Char('x') => {
1292                if let Err(err) = client.workers_cancel_delivery_selected().await {
1293                    client.state.push_log(format!("Deliver: {err}"));
1294                }
1295            }
1296            UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
1297            UiKeyCode::Enter => {
1298                if let Err(err) = client.workers_confirm_action().await {
1299                    client.state.push_log(format!("Worker: {err}"));
1300                }
1301            }
1302            _ => {}
1303        }
1304        return true;
1305    }
1306
1307    false
1308}
1309
1310pub async fn dispatch_action<S: PlayConnection>(
1311    client: &mut GameClient<S>,
1312    _keys: &ClientKeyBindings,
1313    action: InputAction,
1314    ctx: &mut ActionCtx<'_>,
1315) {
1316    match action {
1317        InputAction::StartChat { whisper } => {
1318            if whisper {
1319                // Stone contacts: pick inside CHAT column (no separate popup).
1320                client.state.whisper_pouch_ui.open = false;
1321                client.state.social_chat.picking_stone = true;
1322                client.state.social_chat.stone_pick_index = 0;
1323                client.state.social_chat.input_focused = false;
1324                client
1325                    .state
1326                    .social_chat
1327                    .push_system("Whisper stones — ↑↓ select · Enter · Esc cancel");
1328            } else {
1329                client.state.social_chat.focus_nearby();
1330            }
1331        }
1332        InputAction::Harvest => {
1333            if let Err(err) = client.harvest_nearest().await {
1334                client.state.push_log(format!("Harvest failed: {err}"));
1335            }
1336        }
1337        InputAction::Pickup => {
1338            if client.pickup_nearest().await.is_err() {
1339                if let Err(err) = client.pickup_nearest_container().await {
1340                    client.state.push_log(format!("Pickup: {err}"));
1341                }
1342            }
1343        }
1344        InputAction::Craft => client.open_craft_menu(),
1345        InputAction::Interact | InputAction::UseWorld => {
1346            if let Err(err) = client.use_nearest().await {
1347                client.state.push_log(format!("Use: {err}"));
1348            }
1349        }
1350        InputAction::ClaimPlotBegin => {
1351            if let Err(err) = client.try_begin_claim_mode().await {
1352                client.state.push_log(format!("Claim: {err}"));
1353            }
1354        }
1355        InputAction::FarmCultivate => {
1356            if let Err(err) = client.farm_cultivate_underfoot().await {
1357                client.state.push_log(format!("Cultivate: {err}"));
1358            }
1359        }
1360        InputAction::RenamePlotUnderfoot => {
1361            if let Err(err) = client.open_plot_rename_under_player() {
1362                client.state.push_log(format!("Rename plot: {err}"));
1363            }
1364        }
1365        InputAction::FarmPlant => {
1366            if let Err(err) = client.farm_plant_underfoot().await {
1367                client.state.push_log(format!("Plant: {err}"));
1368            }
1369        }
1370        InputAction::PlotBuild => {
1371            if client.state.show_plot_build_menu {
1372                client.close_plot_build_menu();
1373            } else if let Err(err) = client.open_plot_build_menu() {
1374                client.state.push_log(format!("Build: {err}"));
1375            }
1376        }
1377        InputAction::DoorLockToggle => {
1378            if let Err(err) = client.toggle_nearby_door_lock().await {
1379                client.state.push_log(format!("Door: {err}"));
1380            }
1381        }
1382        InputAction::EnterBuildingDoor => {
1383            if let Err(err) = client.enter_nearby_open_door().await {
1384                client.state.push_log(format!("Enter: {err}"));
1385            }
1386        }
1387        InputAction::ExitBuildingDoor => {
1388            if let Err(err) = client.exit_nearby_building_door().await {
1389                client.state.push_log(format!("Exit: {err}"));
1390            }
1391        }
1392        InputAction::ClaimConfirm => {
1393            if let Err(err) = client.confirm_buy_plot().await {
1394                client.state.push_log(format!("Claim: {err}"));
1395            }
1396        }
1397        InputAction::ClaimCancel => {
1398            client.cancel_claim_mode();
1399        }
1400        InputAction::ClaimNudge { dw, dh } => {
1401            client.claim_nudge(dw, dh);
1402        }
1403        InputAction::ClaimMoveNudge { dx, dy } => {
1404            client.claim_move_nudge(dx, dy);
1405        }
1406        InputAction::ClaimPreset { w, h } => {
1407            client.claim_set_preset(w, h);
1408        }
1409        InputAction::RelocateBeginNearest => {
1410            if let Err(err) = client.try_begin_relocate_nearest() {
1411                client.state.push_log(format!("Relocate: {err}"));
1412            }
1413        }
1414        InputAction::RelocateConfirm => {
1415            if let Err(err) = client.confirm_relocate_container().await {
1416                client.state.push_log(format!("Relocate: {err}"));
1417            }
1418        }
1419        InputAction::RelocateCancel => {
1420            client.cancel_relocate_mode();
1421        }
1422        InputAction::RelocateNudge { dx, dy } => {
1423            client.relocate_nudge(dx, dy);
1424        }
1425        InputAction::TestDamage => {
1426            if let Err(err) = client.test_damage(25.0).await {
1427                client.state.push_log(format!("Damage failed: {err}"));
1428            }
1429        }
1430        InputAction::CycleCombatTarget { reverse } => {
1431            if let Err(err) = client.cycle_combat_target(reverse).await {
1432                client.state.push_log(format!("T1 target: {err}"));
1433            }
1434        }
1435        InputAction::CycleCombatTargetT2 { reverse } => {
1436            if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
1437                client.state.push_log(format!("T2 target: {err}"));
1438            }
1439        }
1440        InputAction::AdvanceRotationT1 => {
1441            if let Err(err) = client.advance_rotation(1).await {
1442                client.state.push_log(format!("T1 step: {err}"));
1443            }
1444        }
1445        InputAction::AdvanceRotationT2 => {
1446            if let Err(err) = client.advance_rotation(2).await {
1447                client.state.push_log(format!("T2 step: {err}"));
1448            }
1449        }
1450        InputAction::ToggleAutoT1 => {
1451            if let Err(err) = client.toggle_auto_attack_slot(1).await {
1452                client.state.push_log(format!("T1 auto: {err}"));
1453            }
1454        }
1455        InputAction::ToggleAutoT2 => {
1456            if let Err(err) = client.toggle_auto_attack_slot(2).await {
1457                client.state.push_log(format!("T2 auto: {err}"));
1458            }
1459        }
1460        InputAction::ToggleLoadout => {
1461            client.state.show_rotation_editor = false;
1462            client.state.rotation_editor.reset();
1463            client.state.show_loadout_menu = !client.state.show_loadout_menu;
1464            if client.state.show_loadout_menu {
1465                client.state.loadout_focus_presets = true;
1466                client.state.loadout_hotbar_slot = client.state.loadout_hotbar_slot.clamp(1, 9);
1467                let ability_len = client.state.loadout_hotbar_choices().len();
1468                if ability_len > 0 {
1469                    client.state.loadout_ability_index =
1470                        client.state.loadout_ability_index.min(ability_len - 1);
1471                } else {
1472                    client.state.loadout_ability_index = 0;
1473                }
1474                let preset_len = client.state.rotation_presets.len();
1475                if preset_len > 0 {
1476                    client.state.loadout_menu_index =
1477                        client.state.loadout_menu_index.min(preset_len - 1);
1478                } else {
1479                    client.state.loadout_menu_index = 0;
1480                }
1481                *ctx.auto_nav = None;
1482                let _ = client.stop().await;
1483            }
1484        }
1485        InputAction::ToggleRotationEditor => {
1486            client.state.show_loadout_menu = false;
1487            if client.state.show_rotation_editor {
1488                client.state.show_rotation_editor = false;
1489                client.state.rotation_editor.reset();
1490            } else {
1491                client.state.show_rotation_editor = true;
1492                client.state.rotation_editor.reset();
1493                let max = client.state.rotation_presets.len();
1494                if max > 0 {
1495                    client.state.rotation_editor.list_index =
1496                        client.state.rotation_editor.list_index.min(max - 1);
1497                }
1498            }
1499            if client.state.show_rotation_editor {
1500                *ctx.auto_nav = None;
1501                let _ = client.stop().await;
1502            }
1503        }
1504        InputAction::LoadoutMenuUp => {
1505            if !client.state.show_loadout_menu {
1506                return;
1507            }
1508            if client.state.loadout_focus_presets {
1509                if client.state.loadout_menu_index > 0 {
1510                    client.state.loadout_menu_index -= 1;
1511                }
1512            } else if client.state.loadout_ability_index > 0 {
1513                client.state.loadout_ability_index -= 1;
1514            }
1515        }
1516        InputAction::LoadoutMenuDown => {
1517            if !client.state.show_loadout_menu {
1518                return;
1519            }
1520            if client.state.loadout_focus_presets {
1521                let max = client.state.rotation_presets.len();
1522                if max > 0 {
1523                    client.state.loadout_menu_index =
1524                        (client.state.loadout_menu_index + 1).min(max - 1);
1525                }
1526            } else {
1527                let max = client.state.loadout_hotbar_choices().len();
1528                if max > 0 {
1529                    client.state.loadout_ability_index =
1530                        (client.state.loadout_ability_index + 1).min(max - 1);
1531                }
1532            }
1533        }
1534        InputAction::LoadoutHotbarPrev => {
1535            if client.state.show_loadout_menu {
1536                let slot = client.state.loadout_hotbar_slot;
1537                client.state.loadout_hotbar_slot = if slot <= 1 { 9 } else { slot - 1 };
1538            }
1539        }
1540        InputAction::LoadoutHotbarNext => {
1541            if client.state.show_loadout_menu {
1542                let slot = client.state.loadout_hotbar_slot;
1543                client.state.loadout_hotbar_slot = if slot >= 9 { 1 } else { slot + 1 };
1544            }
1545        }
1546        InputAction::LoadoutToggleFocus => {
1547            if client.state.show_loadout_menu {
1548                client.state.loadout_focus_presets = !client.state.loadout_focus_presets;
1549            }
1550        }
1551        InputAction::LoadoutBindHotbar => {
1552            if !client.state.show_loadout_menu {
1553                return;
1554            }
1555            let slot = client.state.loadout_hotbar_slot;
1556            let binding = {
1557                let choices = client.state.loadout_hotbar_choices();
1558                choices
1559                    .get(client.state.loadout_ability_index)
1560                    .map(|c| c.binding.clone())
1561            };
1562            if let Some(binding) = binding {
1563                if let Err(err) = client.set_hotbar_slot(slot, Some(&binding)).await {
1564                    client.state.push_log(format!("Hotbar: {err}"));
1565                }
1566            } else {
1567                client
1568                    .state
1569                    .push_log("No ability/consumable selected to bind");
1570            }
1571        }
1572        InputAction::LoadoutClearHotbar => {
1573            if !client.state.show_loadout_menu {
1574                return;
1575            }
1576            let slot = client.state.loadout_hotbar_slot;
1577            if let Err(err) = client.set_hotbar_slot(slot, None).await {
1578                client.state.push_log(format!("Hotbar: {err}"));
1579            }
1580        }
1581        InputAction::LoadoutAssignT1 => {
1582            if !client.state.show_loadout_menu {
1583                return;
1584            }
1585            let preset = {
1586                let idx = client.state.loadout_menu_index;
1587                client.state.rotation_presets.get(idx).cloned()
1588            };
1589            if let Some(preset) = preset {
1590                let already = client
1591                    .state
1592                    .combat_slots
1593                    .iter()
1594                    .find(|s| s.slot_index == 1)
1595                    .and_then(|s| s.preset_id.as_deref())
1596                    == Some(preset.id.as_str());
1597                if already {
1598                    client.state.push_log(format!(
1599                        "T1 already uses {} (equip weapons from inventory — Weapon auto tracks mainhand)",
1600                        preset.label
1601                    ));
1602                } else if let Err(err) = client.assign_slot_preset(1, &preset.id).await {
1603                    client.state.push_log(format!("Loadout: {err}"));
1604                } else {
1605                    client.state.push_log(format!("T1 ← {}", preset.label));
1606                }
1607            }
1608        }
1609        InputAction::LoadoutAssignT2 => {
1610            if !client.state.show_loadout_menu {
1611                return;
1612            }
1613            let preset = {
1614                let idx = client.state.loadout_menu_index;
1615                client.state.rotation_presets.get(idx).cloned()
1616            };
1617            if let Some(preset) = preset {
1618                let already = client
1619                    .state
1620                    .combat_slots
1621                    .iter()
1622                    .find(|s| s.slot_index == 2)
1623                    .and_then(|s| s.preset_id.as_deref())
1624                    == Some(preset.id.as_str());
1625                if already {
1626                    client
1627                        .state
1628                        .push_log(format!("T2 already uses {}", preset.label));
1629                } else if let Err(err) = client.assign_slot_preset(2, &preset.id).await {
1630                    client.state.push_log(format!("Loadout: {err}"));
1631                } else {
1632                    client.state.push_log(format!("T2 ← {}", preset.label));
1633                }
1634            }
1635        }
1636        InputAction::CloseOverlay => {
1637            client.state.show_loadout_menu = false;
1638            client.state.show_rotation_editor = false;
1639            client.state.rotation_editor.reset();
1640        }
1641        InputAction::ClearCombatTarget => {
1642            if let Err(err) = client.clear_combat_target().await {
1643                client.state.push_log(format!("Target: {err}"));
1644            }
1645        }
1646        InputAction::ClearCombatTargetT2 => {
1647            if let Err(err) = client.clear_combat_target_slot(2).await {
1648                client.state.push_log(format!("T2 target: {err}"));
1649            }
1650        }
1651        InputAction::Dodge { forward, strafe } => {
1652            *ctx.auto_nav = None;
1653            if let Err(err) = client.dodge(forward, strafe).await {
1654                client.state.push_log(format!("Dodge: {err}"));
1655            }
1656        }
1657        InputAction::Lunge => {
1658            *ctx.auto_nav = None;
1659            let _ = client.stop().await;
1660            if let Err(err) = client.lunge().await {
1661                client.state.push_log(format!("Lunge: {err}"));
1662            }
1663        }
1664        InputAction::DirectionalJump { forward, strafe } => {
1665            *ctx.auto_nav = None;
1666            let _ = client.stop().await;
1667            if let Err(err) = client.directional_jump(forward, strafe).await {
1668                client.state.push_log(format!("Jump: {err}"));
1669            }
1670        }
1671        InputAction::CastHotbar { slot } => {
1672            if let Err(err) = client.cast_hotbar_ability(slot).await {
1673                client.state.push_log(format!("Hotbar {slot}: {err}"));
1674            }
1675        }
1676        InputAction::ToggleBlock => {
1677            if !client.state.blocking_active {
1678                if let Err(err) = client.set_block(true).await {
1679                    client.state.push_log(format!("Block: {err}"));
1680                }
1681            }
1682        }
1683        InputAction::ToggleStats => client.toggle_stats(),
1684        InputAction::ToggleEquip => client.toggle_equip_menu(),
1685        InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
1686        InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
1687        InputAction::ToggleInventory => client.toggle_inventory_menu(),
1688        InputAction::ToggleKeychain => client.toggle_keychain_menu(),
1689        InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
1690        InputAction::ToggleWorkersMenu => {
1691            if client.state.show_workers_menu {
1692                if let Err(err) = client.close_workers_menu().await {
1693                    client.state.push_log(format!("Worker: {err}"));
1694                }
1695            } else {
1696                client.toggle_workers_menu();
1697            }
1698        }
1699        InputAction::QuestMenuUp => client.quest_menu_move(-1),
1700        InputAction::QuestMenuDown => client.quest_menu_move(1),
1701        InputAction::QuestWithdraw => client.quest_request_withdraw(),
1702        InputAction::RotationEditorBack => {
1703            let _ = client.back_on_esc();
1704        }
1705        InputAction::RotationEditorListUp
1706        | InputAction::RotationEditorListDown
1707        | InputAction::RotationEditorEdit
1708        | InputAction::RotationEditorNew
1709        | InputAction::RotationEditorDelete
1710        | InputAction::RotationEditorAddAbility
1711        | InputAction::RotationEditorRemoveAbility
1712        | InputAction::RotationEditorMoveAbilityUp
1713        | InputAction::RotationEditorMoveAbilityDown
1714        | InputAction::RotationEditorAbilityUp
1715        | InputAction::RotationEditorAbilityDown
1716        | InputAction::RotationEditorPickerUp
1717        | InputAction::RotationEditorPickerDown
1718        | InputAction::RotationEditorPickAbility
1719        | InputAction::RotationEditorRename
1720        | InputAction::RotationEditorConfirmLabel
1721        | InputAction::RotationEditorLabelBackspace
1722        | InputAction::RotationEditorLabelChar(_)
1723        | InputAction::RotationEditorSave => {
1724            handle_rotation_editor_action(client, action).await;
1725        }
1726        InputAction::None
1727        | InputAction::Quit
1728        | InputAction::ToggleHelp
1729        | InputAction::CycleHudView
1730        | InputAction::SubmitChat
1731        | InputAction::CancelChat
1732        | InputAction::ToggleSprintMode
1733        | InputAction::ToggleMapTarget
1734        | InputAction::ConfirmMapTarget
1735        | InputAction::CancelMapTarget
1736        | InputAction::MapTargetNudge { .. }
1737        | InputAction::CancelAutoNav
1738        | InputAction::StopMovement => {}
1739        InputAction::ToggleHudLog => {
1740            let hud_view = flatland_client_lib::ClientConfig::load()
1741                .hud_view
1742                .as_deref()
1743                .and_then(flatland_client_ui::HudViewMode::from_label)
1744                .unwrap_or_default();
1745            if hud_view != flatland_client_ui::HudViewMode::Normal {
1746                client
1747                    .state
1748                    .push_log("LOG hide/show only works in normal HUD (press .)");
1749                return;
1750            }
1751            client.state.hud_log_hidden = !client.state.hud_log_hidden;
1752            let mut cfg = flatland_client_lib::ClientConfig::load();
1753            let _ = cfg.save_hud_log_hidden(client.state.hud_log_hidden);
1754            if client.state.hud_log_hidden {
1755                client
1756                    .state
1757                    .push_log("LOG hidden — press ' to show (normal HUD)");
1758            } else {
1759                client.state.push_log("LOG shown — press ' to hide");
1760            }
1761        }
1762    }
1763}
1764
1765async fn handle_rotation_editor_action<S: PlayConnection>(
1766    client: &mut GameClient<S>,
1767    action: InputAction,
1768) {
1769    match action {
1770        InputAction::RotationEditorListUp => {
1771            if client.state.rotation_editor.list_index > 0 {
1772                client.state.rotation_editor.list_index -= 1;
1773            }
1774        }
1775        InputAction::RotationEditorListDown => {
1776            let max = client.state.rotation_presets.len();
1777            if max > 0 {
1778                client.state.rotation_editor.list_index =
1779                    (client.state.rotation_editor.list_index + 1).min(max - 1);
1780            }
1781        }
1782        InputAction::RotationEditorEdit => {
1783            let idx = client.state.rotation_editor.list_index;
1784            if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
1785                client.state.rotation_editor.draft = Some(preset);
1786                client.state.rotation_editor.ability_index = 0;
1787                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1788            }
1789        }
1790        InputAction::RotationEditorNew => {
1791            let preset = next_custom_preset(&client.state.rotation_presets);
1792            client.state.rotation_editor.list_index = client.state.rotation_presets.len();
1793            client.state.rotation_editor.draft = Some(preset);
1794            client.state.rotation_editor.ability_index = 0;
1795            client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1796        }
1797        InputAction::RotationEditorDelete => {
1798            let idx = client.state.rotation_editor.list_index;
1799            let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
1800            if let Some(id) = preset_id {
1801                if preset_deletable(&id) {
1802                    if let Err(err) = client.delete_rotation_preset(&id).await {
1803                        client.state.push_log(format!("Delete: {err}"));
1804                    } else {
1805                        let max = client.state.rotation_presets.len();
1806                        client.state.rotation_editor.list_index = if max == 0 {
1807                            0
1808                        } else {
1809                            client.state.rotation_editor.list_index.min(max - 1)
1810                        };
1811                    }
1812                } else {
1813                    client.state.push_log("Cannot delete built-in preset");
1814                }
1815            }
1816        }
1817        InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
1818            RotationEditorMode::EditLabel => {
1819                client.state.rotation_editor.label_buffer.clear();
1820                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1821            }
1822            RotationEditorMode::PickAbility => {
1823                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1824            }
1825            RotationEditorMode::EditSequence => {
1826                client.state.rotation_editor.draft = None;
1827                client.state.rotation_editor.mode = RotationEditorMode::List;
1828            }
1829            RotationEditorMode::List => {}
1830        },
1831        InputAction::RotationEditorAddAbility => {
1832            client.state.rotation_editor.picker_index = 0;
1833            client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
1834        }
1835        InputAction::RotationEditorRemoveAbility => {
1836            let idx = client.state.rotation_editor.ability_index;
1837            if let Some(draft) = &mut client.state.rotation_editor.draft {
1838                if idx < draft.abilities.len() {
1839                    draft.abilities.remove(idx);
1840                    if client.state.rotation_editor.ability_index >= draft.abilities.len()
1841                        && client.state.rotation_editor.ability_index > 0
1842                    {
1843                        client.state.rotation_editor.ability_index -= 1;
1844                    }
1845                }
1846            }
1847        }
1848        InputAction::RotationEditorMoveAbilityUp => {
1849            let i = client.state.rotation_editor.ability_index;
1850            if let Some(draft) = &mut client.state.rotation_editor.draft {
1851                if i > 0 && i < draft.abilities.len() {
1852                    draft.abilities.swap(i, i - 1);
1853                    client.state.rotation_editor.ability_index -= 1;
1854                }
1855            }
1856        }
1857        InputAction::RotationEditorMoveAbilityDown => {
1858            let i = client.state.rotation_editor.ability_index;
1859            if let Some(draft) = &mut client.state.rotation_editor.draft {
1860                if i + 1 < draft.abilities.len() {
1861                    draft.abilities.swap(i, i + 1);
1862                    client.state.rotation_editor.ability_index += 1;
1863                }
1864            }
1865        }
1866        InputAction::RotationEditorAbilityUp => {
1867            if client.state.rotation_editor.ability_index > 0 {
1868                client.state.rotation_editor.ability_index -= 1;
1869            }
1870        }
1871        InputAction::RotationEditorAbilityDown => {
1872            if let Some(draft) = &client.state.rotation_editor.draft {
1873                if !draft.abilities.is_empty() {
1874                    client.state.rotation_editor.ability_index =
1875                        (client.state.rotation_editor.ability_index + 1)
1876                            .min(draft.abilities.len() - 1);
1877                }
1878            }
1879        }
1880        InputAction::RotationEditorPickerUp => {
1881            if client.state.rotation_editor.picker_index > 0 {
1882                client.state.rotation_editor.picker_index -= 1;
1883            }
1884        }
1885        InputAction::RotationEditorPickerDown => {
1886            let choices = editor_ability_choices(
1887                &client.state.known_abilities,
1888                &client.state.weapon_ability_id,
1889                |id| client.state.ability_auto_rotation_eligible(id),
1890            );
1891            if !choices.is_empty() {
1892                client.state.rotation_editor.picker_index =
1893                    (client.state.rotation_editor.picker_index + 1).min(choices.len() - 1);
1894            }
1895        }
1896        InputAction::RotationEditorPickAbility => {
1897            let choices = editor_ability_choices(
1898                &client.state.known_abilities,
1899                &client.state.weapon_ability_id,
1900                |id| client.state.ability_auto_rotation_eligible(id),
1901            );
1902            let pick = client.state.rotation_editor.picker_index;
1903            if let Some(ability) = choices.get(pick) {
1904                if let Some(draft) = &mut client.state.rotation_editor.draft {
1905                    let max = client.state.max_abilities_per_rotation.max(1) as usize;
1906                    if draft.abilities.len() >= max {
1907                        client
1908                            .state
1909                            .push_log(format!("Rotation full (max {max} from INT+WIS)"));
1910                    } else {
1911                        draft.abilities.push(ability.clone());
1912                        client.state.rotation_editor.ability_index =
1913                            draft.abilities.len().saturating_sub(1);
1914                    }
1915                }
1916                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1917            }
1918        }
1919        InputAction::RotationEditorRename => {
1920            if let Some(draft) = &client.state.rotation_editor.draft {
1921                client.state.rotation_editor.label_buffer = draft.label.clone();
1922                client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
1923            }
1924        }
1925        InputAction::RotationEditorConfirmLabel => {
1926            let label = client.state.rotation_editor.label_buffer.trim().to_string();
1927            if label.is_empty() {
1928                client.state.push_log("Label cannot be empty");
1929            } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1930                draft.label = label;
1931                client.state.rotation_editor.label_buffer.clear();
1932                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1933            }
1934        }
1935        InputAction::RotationEditorLabelBackspace => {
1936            client.state.rotation_editor.label_buffer.pop();
1937        }
1938        InputAction::RotationEditorLabelChar(c) => {
1939            if client.state.rotation_editor.label_buffer.len() < 32 {
1940                client.state.rotation_editor.label_buffer.push(c);
1941            }
1942        }
1943        InputAction::RotationEditorSave => {
1944            let draft = match client.state.rotation_editor.draft.clone() {
1945                Some(d) => d,
1946                None => return,
1947            };
1948            if draft.abilities.is_empty() {
1949                client.state.push_log("Rotation needs at least one ability");
1950                return;
1951            }
1952            let saved_id = draft.id.clone();
1953            if let Err(err) = client.upsert_rotation_preset(draft).await {
1954                client.state.push_log(format!("Save: {err}"));
1955            } else {
1956                client.state.rotation_editor.mode = RotationEditorMode::List;
1957                client.state.rotation_editor.draft = None;
1958                if let Some(i) = client
1959                    .state
1960                    .rotation_presets
1961                    .iter()
1962                    .position(|p| p.id == saved_id)
1963                {
1964                    client.state.rotation_editor.list_index = i;
1965                }
1966            }
1967        }
1968        _ => {}
1969    }
1970}