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