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