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