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::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 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 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 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 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 } 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 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 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 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 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 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}