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 if client.state.trade_ui.picking_inventory {
557 let max = client.state.inventory_stacks.len().saturating_sub(1);
558 client.state.trade_ui.inventory_index =
559 client.state.trade_ui.inventory_index.min(max);
560 }
561 }
562 UiKeyCode::Up | UiKeyCode::Char('k') => {
563 if client.state.trade_ui.picking_inventory {
564 if client.state.trade_ui.inventory_index > 0 {
565 client.state.trade_ui.inventory_index -= 1;
566 }
567 } else if client.state.trade_ui.select_index > 0 {
568 client.state.trade_ui.select_index -= 1;
569 }
570 }
571 UiKeyCode::Down | UiKeyCode::Char('j') => {
572 if client.state.trade_ui.picking_inventory {
573 let max = client.state.inventory_stacks.len().saturating_sub(1);
574 if client.state.trade_ui.inventory_index < max {
575 client.state.trade_ui.inventory_index += 1;
576 }
577 }
578 }
579 UiKeyCode::PageUp => {
580 if client.state.trade_ui.picking_inventory {
581 let n = client.state.inventory_stacks.len();
582 client.state.trade_ui.inventory_index =
583 flatland_client_lib::page_list_index(
584 client.state.trade_ui.inventory_index,
585 -1,
586 n,
587 );
588 }
589 }
590 UiKeyCode::PageDown => {
591 if client.state.trade_ui.picking_inventory {
592 let n = client.state.inventory_stacks.len();
593 client.state.trade_ui.inventory_index =
594 flatland_client_lib::page_list_index(
595 client.state.trade_ui.inventory_index,
596 1,
597 n,
598 );
599 }
600 }
601 UiKeyCode::Enter => {
602 if client.state.trade_ui.picking_inventory {
603 if let Err(err) = client.trade_confirm_qty_or_present().await {
604 client.state.push_log(format!("Present failed: {err}"));
605 }
606 }
607 }
608 _ => {}
609 }
610 return true;
611 }
612
613 if client.state.whisper_pouch_ui.open {
614 let contacts =
615 flatland_client_lib::contacts_from_stacks(&client.state.whisper_pouch_stacks);
616 match key.code {
617 UiKeyCode::Esc => client.state.whisper_pouch_ui.open = false,
618 UiKeyCode::Up | UiKeyCode::Char('k') => {
619 if client.state.whisper_pouch_ui.index > 0 {
620 client.state.whisper_pouch_ui.index -= 1;
621 }
622 }
623 UiKeyCode::Down | UiKeyCode::Char('j') => {
624 let max = contacts.len().saturating_sub(1);
625 if client.state.whisper_pouch_ui.index < max {
626 client.state.whisper_pouch_ui.index += 1;
627 }
628 }
629 UiKeyCode::Enter => {
630 if let Some(c) = contacts.get(client.state.whisper_pouch_ui.index) {
631 if !c.blank {
632 if let Some(peer) = client
634 .state
635 .entities
636 .iter()
637 .find(|e| e.id != client.state.entity_id && e.label == c.peer_label)
638 {
639 client
640 .state
641 .social_chat
642 .open_stone(peer.id, &c.peer_label);
643 client.state.whisper_pouch_ui.open = false;
644 } else {
645 client.state.push_log(format!(
646 "{} is not online in this region",
647 c.peer_label
648 ));
649 }
650 }
651 }
652 }
653 UiKeyCode::Char('x') | UiKeyCode::Char('d') => {
654 if let Some(c) = contacts.get(client.state.whisper_pouch_ui.index) {
655 let id = c.instance_id;
656 if let Err(err) = client.destroy_whisper_stone(id).await {
657 client.state.push_log(format!("Destroy stone failed: {err}"));
658 }
659 }
660 }
661 _ => {}
662 }
663 return true;
664 }
665
666 if client.state.bank_panel.is_some() {
667 match &client.state.bank_ui_mode {
668 flatland_client_lib::BankUiMode::Menu => match key.code {
669 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
670 if let Err(err) = client.close_bank_panel().await {
671 client.state.push_log(format!("Bank close failed: {err}"));
672 }
673 }
674 UiKeyCode::Up | UiKeyCode::Char('k') => client.bank_menu_move(-1),
675 UiKeyCode::Down | UiKeyCode::Char('j') => client.bank_menu_move(1),
676 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
677 if let Err(err) = client.confirm_bank_menu().await {
678 client.state.push_log(format!("Bank action failed: {err}"));
679 }
680 }
681 UiKeyCode::Char('d') => {
682 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::DepositAmount {
683 input: String::new(),
684 };
685 }
686 UiKeyCode::Char('w') => {
687 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::WithdrawAmount {
688 input: String::new(),
689 };
690 }
691 UiKeyCode::Char('t') => {
692 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::TransferName {
693 input: String::new(),
694 };
695 }
696 _ => {}
697 },
698 _ => match key.code {
699 UiKeyCode::Esc | UiKeyCode::Char('[') => client.bank_transfer_back(),
700 UiKeyCode::Enter => {
701 if let Err(err) = client.confirm_bank_menu().await {
702 client.state.push_log(format!("Bank action failed: {err}"));
703 }
704 }
705 UiKeyCode::Backspace => client.bank_transfer_backspace(),
706 UiKeyCode::Char(c) => client.bank_transfer_append_char(c),
707 _ => {}
708 },
709 }
710 return true;
711 }
712
713 if client.state.storage_panel.is_some() {
714 match &client.state.storage_ui_mode {
715 flatland_client_lib::StorageUiMode::Menu => match key.code {
716 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
717 if let Err(err) = client.close_storage_panel().await {
718 client.state.push_log(format!("Storage close failed: {err}"));
719 }
720 }
721 UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_menu_move(-1),
722 UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_menu_move(1),
723 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
724 if let Err(err) = client.confirm_storage_menu().await {
725 client.state.push_log(format!("Storage action failed: {err}"));
726 }
727 }
728 UiKeyCode::Char('s') => {
729 let opts = client.state.storage_store_options();
730 if opts.is_empty() {
731 client.state.push_log("Nothing loose to store.");
732 } else {
733 client.state.storage_ui_mode =
734 flatland_client_lib::StorageUiMode::StorePick { index: 0 };
735 }
736 }
737 UiKeyCode::Char('t') => {
738 let opts = client.state.storage_vault_options();
739 if opts.is_empty() {
740 client.state.push_log("Vault is empty.");
741 } else {
742 client.state.storage_ui_mode =
743 flatland_client_lib::StorageUiMode::TakePick { index: 0 };
744 }
745 }
746 _ => {}
747 },
748 flatland_client_lib::StorageUiMode::StoreAmount { .. }
749 | flatland_client_lib::StorageUiMode::TakeAmount { .. }
750 | flatland_client_lib::StorageUiMode::ShipAmount { .. } => match key.code {
751 UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
752 UiKeyCode::Enter => {
753 if let Err(err) = client.confirm_storage_menu().await {
754 client.state.push_log(format!("Storage action failed: {err}"));
755 }
756 }
757 UiKeyCode::Backspace => client.storage_amount_backspace(),
758 UiKeyCode::Char(c) => client.storage_amount_append_char(c),
759 _ => {}
760 },
761 _ => match key.code {
762 UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
763 UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_pick_move(-1),
764 UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_pick_move(1),
765 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
766 if let Err(err) = client.confirm_storage_menu().await {
767 client.state.push_log(format!("Storage action failed: {err}"));
768 }
769 }
770 _ => {}
771 },
772 }
773 return true;
774 }
775
776 if client.state.market_panel.is_some() {
777 let filter_focused = client.state.market_filter_focused;
778 match &client.state.market_ui_mode {
779 flatland_client_lib::MarketUiMode::Browse => {
780 if filter_focused {
781 match key.code {
782 UiKeyCode::Esc => {
783 let _ = client.clear_or_blur_market_filter();
784 }
785 UiKeyCode::Enter => {
786 client.state.market_filter_focused = false;
787 }
788 UiKeyCode::Backspace => client.market_filter_backspace(),
789 UiKeyCode::Char(c) => client.append_market_filter_char(c),
790 _ => {}
791 }
792 return true;
793 }
794 match key.code {
795 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
796 if client.state.market_buy_confirm.is_some() {
797 client.state.market_buy_confirm = None;
798 } else if client.clear_or_blur_market_filter() {
799 } else if let Err(err) = client.close_market_panel().await {
801 client.state.push_log(format!("Market close failed: {err}"));
802 }
803 }
804 UiKeyCode::Up | UiKeyCode::Char('k') => {
805 if client.state.market_buy_confirm.is_none() {
806 client.market_move_selection(-1);
807 }
808 }
809 UiKeyCode::Down | UiKeyCode::Char('j') => {
810 if client.state.market_buy_confirm.is_none() {
811 client.market_move_selection(1);
812 }
813 }
814 UiKeyCode::PageUp => {
815 if client.state.market_buy_confirm.is_none() {
816 client.market_page_selection(-1);
817 }
818 }
819 UiKeyCode::PageDown => {
820 if client.state.market_buy_confirm.is_none() {
821 client.market_page_selection(1);
822 }
823 }
824 UiKeyCode::Tab => {
825 if client.state.market_buy_confirm.is_none() {
826 client.market_cycle_category(1);
827 }
828 }
829 UiKeyCode::Char('/') => client.focus_market_filter(),
830 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
831 if let Err(err) = client.market_activate_selection().await {
832 client.state.push_log(format!("Market action failed: {err}"));
833 }
834 }
835 UiKeyCode::Char('l') => client.market_begin_list(),
836 _ => {}
837 }
838 }
839 flatland_client_lib::MarketUiMode::ListAmount { .. }
840 | flatland_client_lib::MarketUiMode::ListPrice { .. } => match key.code {
841 UiKeyCode::Esc | UiKeyCode::Char('[') => client.market_ui_back(),
842 UiKeyCode::Enter => {
843 if let Err(err) = client.confirm_market_list_step().await {
844 client.state.push_log(format!("Market list failed: {err}"));
845 }
846 }
847 UiKeyCode::Backspace => client.market_list_amount_backspace(),
848 UiKeyCode::Char(c) => client.market_list_amount_append_char(c),
849 _ => {}
850 },
851 _ => {
852 if filter_focused {
853 match key.code {
854 UiKeyCode::Esc => {
855 let _ = client.clear_or_blur_market_filter();
856 }
857 UiKeyCode::Enter => {
858 client.state.market_filter_focused = false;
859 }
860 UiKeyCode::Backspace => client.market_filter_backspace(),
861 UiKeyCode::Char(c) => client.append_market_filter_char(c),
862 _ => {}
863 }
864 return true;
865 }
866 match key.code {
867 UiKeyCode::Esc | UiKeyCode::Char('[') => {
868 if !client.clear_or_blur_market_filter() {
869 client.market_ui_back();
870 }
871 }
872 UiKeyCode::Up | UiKeyCode::Char('k') => client.market_list_move(-1),
873 UiKeyCode::Down | UiKeyCode::Char('j') => client.market_list_move(1),
874 UiKeyCode::PageUp => client.market_list_page(-1),
875 UiKeyCode::PageDown => client.market_list_page(1),
876 UiKeyCode::Tab => client.market_cycle_category(1),
877 UiKeyCode::Char('/') => client.focus_market_filter(),
878 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
879 if let Err(err) = client.confirm_market_list_step().await {
880 client.state.push_log(format!("Market list failed: {err}"));
881 }
882 }
883 _ => {}
884 }
885 }
886 }
887 return true;
888 }
889
890 if client.state.show_shop_menu {
891 match key.code {
892 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
893 if let Err(err) = client.back_from_shop_menu().await {
894 client.state.push_log(format!("Shop close failed: {err}"));
895 }
896 }
897 UiKeyCode::Tab => client.shop_tab_toggle(),
898 UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
899 UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
900 UiKeyCode::PageUp => client.shop_menu_page(-1),
901 UiKeyCode::PageDown => client.shop_menu_page(1),
902 UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
903 UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
904 UiKeyCode::Char('a') => client.shop_quantity_set_max(),
905 UiKeyCode::Enter => {
906 if let Err(err) = client.shop_confirm().await {
907 client.state.push_log(format!("Trade failed: {err}"));
908 }
909 }
910 _ => {}
911 }
912 return true;
913 }
914
915 if client.state.show_quest_menu {
916 match key.code {
917 UiKeyCode::Esc => {
918 if client.state.quest_withdraw_confirm {
919 client.state.quest_withdraw_confirm = false;
920 } else {
921 client.state.show_quest_menu = false;
922 }
923 }
924 UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
925 UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
926 UiKeyCode::PageUp => client.quest_menu_page(-1),
927 UiKeyCode::PageDown => client.quest_menu_page(1),
928 UiKeyCode::Char('x') => client.quest_request_withdraw(),
929 UiKeyCode::Enter => {
930 if let Err(err) = client.quest_confirm_action().await {
931 client.state.push_log(format!("Quest: {err}"));
932 }
933 }
934 _ => {}
935 }
936 return true;
937 }
938
939 if client.state.worker_route_editor.is_some() {
940 let at_root = client.re_at_root_sheet();
941 if at_root {
942 match key.code {
944 UiKeyCode::Esc => client.close_worker_route_editor(),
945 UiKeyCode::Char('s') => {
946 if let Err(err) = client.worker_route_editor_save().await {
947 client.state.push_log(format!("Route: {err}"));
948 }
949 }
950 UiKeyCode::Down => {
951 if key.modifiers.contains_control() {
952 client.worker_route_editor_move_selected(1);
953 } else {
954 client.worker_route_editor_select(1);
955 }
956 }
957 UiKeyCode::Up => {
958 if key.modifiers.contains_control() {
959 client.worker_route_editor_move_selected(-1);
960 } else {
961 client.worker_route_editor_select(-1);
962 }
963 }
964 UiKeyCode::Char('j') => {
967 if key.modifiers.contains_control() {
968 client.worker_route_editor_move_selected(-1);
969 } else {
970 client.worker_route_editor_select(1);
971 }
972 }
973 UiKeyCode::Char('k') => {
974 if key.modifiers.contains_control() {
975 client.worker_route_editor_move_selected(1);
976 } else {
977 client.worker_route_editor_select(-1);
978 }
979 }
980 UiKeyCode::Char('d') | UiKeyCode::Delete => {
981 client.worker_route_editor_delete_selected()
982 }
983 UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
984 UiKeyCode::Char('a') => client.re_open_add_menu(),
985 UiKeyCode::Char('l') => client.re_open_bed_picker(),
986 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
987 UiKeyCode::Enter => client.re_edit_selected_stop(),
988 _ => {}
989 }
990 } else {
991 let filter_focused = client
993 .state
994 .worker_route_editor
995 .as_ref()
996 .map(|ed| ed.sheet_filter_focused)
997 .unwrap_or(false);
998 let sheet_index = client.re_sheet_index();
999
1000 if filter_focused {
1001 match key.code {
1003 UiKeyCode::Esc => {
1004 let _ = client.clear_or_blur_re_sheet_filter();
1005 }
1006 UiKeyCode::Enter => client.re_blur_sheet_filter_keep_text(),
1007 UiKeyCode::Backspace => client.re_sheet_filter_backspace(),
1008 UiKeyCode::Char(c) => client.re_append_sheet_filter_char(c),
1009 _ => {}
1010 }
1011 } else {
1012 match key.code {
1013 UiKeyCode::Esc => {
1014 if !client.clear_or_blur_re_sheet_filter() {
1015 client.re_sheet_back();
1016 }
1017 }
1018 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
1019 UiKeyCode::PageUp => client.re_sheet_page(-1),
1020 UiKeyCode::PageDown => client.re_sheet_page(1),
1021 UiKeyCode::Char('/') => client.re_focus_sheet_filter(),
1022 UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
1023 UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
1024 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
1025 client.re_sheet_row_activate(sheet_index)
1026 }
1027 UiKeyCode::Char('[') | UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
1028 UiKeyCode::Char(']') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
1029 _ => {}
1030 }
1031 }
1032 }
1033 return true;
1034 }
1035
1036 if client.state.show_plant_menu {
1037 match key.code {
1038 UiKeyCode::Esc => client.close_plant_menu(),
1039 UiKeyCode::Up | UiKeyCode::Char('k') => client.plant_menu_move(-1),
1040 UiKeyCode::Down | UiKeyCode::Char('j') => client.plant_menu_move(1),
1041 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
1042 client.plant_menu_adjust_quantity(-1)
1043 }
1044 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
1045 client.plant_menu_adjust_quantity(1)
1046 }
1047 UiKeyCode::Char('a') | UiKeyCode::Char('A') => client.plant_menu_set_quantity_max(),
1048 UiKeyCode::Enter | UiKeyCode::Char('f') | UiKeyCode::Char('F') => {
1049 if let Err(err) = client.confirm_plant_menu().await {
1050 client.state.push_log(format!("Plant: {err}"));
1051 }
1052 }
1053 _ => {}
1054 }
1055 return true;
1056 }
1057
1058 if client.state.show_farm_access {
1059 match key.code {
1060 UiKeyCode::Esc => client.close_farm_access_panel(),
1061 UiKeyCode::Up | UiKeyCode::Char('k') => client.farm_access_move(-1),
1062 UiKeyCode::Down | UiKeyCode::Char('j') => client.farm_access_move(1),
1063 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
1064 if let Err(err) = client.farm_access_activate().await {
1065 client.state.push_log(format!("Farm access: {err}"));
1066 }
1067 }
1068 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
1069 if let Err(err) = client.farm_access_adjust_discount(-100).await {
1070 client.state.push_log(format!("Farm access: {err}"));
1071 }
1072 }
1073 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
1074 if let Err(err) = client.farm_access_adjust_discount(100).await {
1075 client.state.push_log(format!("Farm access: {err}"));
1076 }
1077 }
1078 _ => {}
1079 }
1080 return true;
1081 }
1082
1083 if client.state.show_worker_give_picker {
1084 match key.code {
1085 UiKeyCode::Esc => client.close_worker_give_picker(),
1086 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
1087 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
1088 UiKeyCode::Enter => {
1089 if let Err(err) = client.confirm_worker_give_picker().await {
1090 client.state.push_log(format!("Give: {err}"));
1091 }
1092 }
1093 _ => {}
1094 }
1095 return true;
1096 }
1097
1098 if client.state.show_worker_give_target_picker {
1099 match key.code {
1100 UiKeyCode::Esc => client.close_worker_give_target_picker(),
1101 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_target_picker_move(-1),
1102 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_target_picker_move(1),
1103 UiKeyCode::Enter => {
1104 if let Err(err) = client.confirm_worker_give_target_picker().await {
1105 client.state.push_log(format!("Give: {err}"));
1106 }
1107 }
1108 _ => {}
1109 }
1110 return true;
1111 }
1112
1113 if client.state.show_worker_take_picker {
1114 match key.code {
1115 UiKeyCode::Esc => client.close_worker_take_picker(),
1116 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_take_picker_move(-1),
1117 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_take_picker_move(1),
1118 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
1119 client.worker_take_picker_adjust_quantity(-1)
1120 }
1121 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
1122 client.worker_take_picker_adjust_quantity(1)
1123 }
1124 UiKeyCode::Char('a') => client.worker_take_picker_set_quantity_max(),
1125 UiKeyCode::Enter => {
1126 if let Err(err) = client.confirm_worker_take_picker().await {
1127 client.state.push_log(format!("Take: {err}"));
1128 }
1129 }
1130 _ => {}
1131 }
1132 return true;
1133 }
1134
1135 if client.state.show_worker_teach_picker {
1136 match key.code {
1137 UiKeyCode::Esc => client.close_worker_teach_picker(),
1138 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
1139 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
1140 UiKeyCode::Enter => {
1141 if let Err(err) = client.confirm_worker_teach_picker().await {
1142 client.state.push_log(format!("Teach: {err}"));
1143 }
1144 }
1145 _ => {}
1146 }
1147 return true;
1148 }
1149
1150 if client.state.show_workers_menu {
1151 match key.code {
1152 UiKeyCode::Esc => {
1153 if let Err(err) = client.close_workers_menu().await {
1154 client.state.push_log(format!("Worker: {err}"));
1155 }
1156 }
1157 UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
1158 UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
1159 UiKeyCode::PageUp => client.workers_menu_page(-1),
1160 UiKeyCode::PageDown => client.workers_menu_page(1),
1161 UiKeyCode::Char('d') => {
1162 if let Err(err) = client.workers_dismiss_selected().await {
1163 client.state.push_log(format!("Worker: {err}"));
1164 }
1165 }
1166 UiKeyCode::Char('r') => {
1167 if let Err(err) = client.hire_worker_laborer().await {
1168 client.state.push_log(format!("Hire: {err}"));
1169 }
1170 }
1171 UiKeyCode::Char('e') => {
1172 if let Err(err) = client.open_worker_route_editor_for_selected() {
1173 client.state.push_log(format!("Route: {err}"));
1174 }
1175 }
1176 UiKeyCode::Char('n') => {
1177 if let Err(err) = client.open_worker_rename() {
1178 client.state.push_log(format!("Rename: {err}"));
1179 }
1180 }
1181 UiKeyCode::Char('g') => {
1182 if let Err(err) = client.open_worker_give_picker() {
1183 client.state.push_log(format!("Give: {err}"));
1184 }
1185 }
1186 UiKeyCode::Char('i') => {
1187 if let Err(err) = client.open_worker_take_picker() {
1188 client.state.push_log(format!("Take: {err}"));
1189 }
1190 }
1191 UiKeyCode::Char('t') => {
1192 if let Err(err) = client.open_worker_teach_picker() {
1193 client.state.push_log(format!("Teach: {err}"));
1194 }
1195 }
1196 UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
1197 UiKeyCode::Enter => {
1198 if let Err(err) = client.workers_confirm_action().await {
1199 client.state.push_log(format!("Worker: {err}"));
1200 }
1201 }
1202 _ => {}
1203 }
1204 return true;
1205 }
1206
1207 false
1208}
1209
1210pub async fn dispatch_action<S: PlayConnection>(
1211 client: &mut GameClient<S>,
1212 _keys: &ClientKeyBindings,
1213 action: InputAction,
1214 ctx: &mut ActionCtx<'_>,
1215) {
1216 match action {
1217 InputAction::StartChat { whisper } => {
1218 if whisper {
1219 client.state.whisper_pouch_ui.open = false;
1221 client.state.social_chat.picking_stone = true;
1222 client.state.social_chat.stone_pick_index = 0;
1223 client.state.social_chat.input_focused = false;
1224 client.state.social_chat.push_system(
1225 "Whisper stones — ↑↓ select · Enter · Esc cancel",
1226 );
1227 } else {
1228 client.state.social_chat.focus_nearby();
1229 }
1230 }
1231 InputAction::Harvest => {
1232 if let Err(err) = client.harvest_nearest().await {
1233 client.state.push_log(format!("Harvest failed: {err}"));
1234 }
1235 }
1236 InputAction::Pickup => {
1237 if client.pickup_nearest().await.is_err() {
1238 if let Err(err) = client.pickup_nearest_container().await {
1239 client.state.push_log(format!("Pickup: {err}"));
1240 }
1241 }
1242 }
1243 InputAction::Craft => client.open_craft_menu(),
1244 InputAction::Interact | InputAction::UseWorld => {
1245 if let Err(err) = client.use_nearest().await {
1246 client.state.push_log(format!("Use: {err}"));
1247 }
1248 }
1249 InputAction::ClaimPlotBegin => {
1250 if let Err(err) = client.try_begin_claim_mode().await {
1251 client.state.push_log(format!("Claim: {err}"));
1252 }
1253 }
1254 InputAction::FarmCultivate => {
1255 if let Err(err) = client.farm_cultivate_underfoot().await {
1256 client.state.push_log(format!("Cultivate: {err}"));
1257 }
1258 }
1259 InputAction::FarmPlant => {
1260 if let Err(err) = client.farm_plant_underfoot().await {
1261 client.state.push_log(format!("Plant: {err}"));
1262 }
1263 }
1264 InputAction::ClaimConfirm => {
1265 if let Err(err) = client.confirm_buy_plot().await {
1266 client.state.push_log(format!("Claim: {err}"));
1267 }
1268 }
1269 InputAction::ClaimCancel => {
1270 client.cancel_claim_mode();
1271 }
1272 InputAction::ClaimNudge { dw, dh } => {
1273 client.claim_nudge(dw, dh);
1274 }
1275 InputAction::ClaimMoveNudge { dx, dy } => {
1276 client.claim_move_nudge(dx, dy);
1277 }
1278 InputAction::ClaimPreset { w, h } => {
1279 client.claim_set_preset(w, h);
1280 }
1281 InputAction::RelocateBeginNearest => {
1282 if let Err(err) = client.try_begin_relocate_nearest() {
1283 client.state.push_log(format!("Relocate: {err}"));
1284 }
1285 }
1286 InputAction::RelocateConfirm => {
1287 if let Err(err) = client.confirm_relocate_container().await {
1288 client.state.push_log(format!("Relocate: {err}"));
1289 }
1290 }
1291 InputAction::RelocateCancel => {
1292 client.cancel_relocate_mode();
1293 }
1294 InputAction::RelocateNudge { dx, dy } => {
1295 client.relocate_nudge(dx, dy);
1296 }
1297 InputAction::TestDamage => {
1298 if let Err(err) = client.test_damage(25.0).await {
1299 client.state.push_log(format!("Damage failed: {err}"));
1300 }
1301 }
1302 InputAction::CycleCombatTarget { reverse } => {
1303 if let Err(err) = client.cycle_combat_target(reverse).await {
1304 client.state.push_log(format!("T1 target: {err}"));
1305 }
1306 }
1307 InputAction::CycleCombatTargetT2 { reverse } => {
1308 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
1309 client.state.push_log(format!("T2 target: {err}"));
1310 }
1311 }
1312 InputAction::AdvanceRotationT1 => {
1313 if let Err(err) = client.advance_rotation(1).await {
1314 client.state.push_log(format!("T1 step: {err}"));
1315 }
1316 }
1317 InputAction::AdvanceRotationT2 => {
1318 if let Err(err) = client.advance_rotation(2).await {
1319 client.state.push_log(format!("T2 step: {err}"));
1320 }
1321 }
1322 InputAction::ToggleAutoT1 => {
1323 if let Err(err) = client.toggle_auto_attack_slot(1).await {
1324 client.state.push_log(format!("T1 auto: {err}"));
1325 }
1326 }
1327 InputAction::ToggleAutoT2 => {
1328 if let Err(err) = client.toggle_auto_attack_slot(2).await {
1329 client.state.push_log(format!("T2 auto: {err}"));
1330 }
1331 }
1332 InputAction::ToggleLoadout => {
1333 client.state.show_rotation_editor = false;
1334 client.state.rotation_editor.reset();
1335 client.state.show_loadout_menu = !client.state.show_loadout_menu;
1336 if client.state.show_loadout_menu {
1337 client.state.loadout_focus_presets = true;
1338 client.state.loadout_hotbar_slot = client.state.loadout_hotbar_slot.clamp(1, 9);
1339 let ability_len = client.state.loadout_hotbar_choices().len();
1340 if ability_len > 0 {
1341 client.state.loadout_ability_index =
1342 client.state.loadout_ability_index.min(ability_len - 1);
1343 } else {
1344 client.state.loadout_ability_index = 0;
1345 }
1346 let preset_len = client.state.rotation_presets.len();
1347 if preset_len > 0 {
1348 client.state.loadout_menu_index =
1349 client.state.loadout_menu_index.min(preset_len - 1);
1350 } else {
1351 client.state.loadout_menu_index = 0;
1352 }
1353 *ctx.auto_nav = None;
1354 let _ = client.stop().await;
1355 }
1356 }
1357 InputAction::ToggleRotationEditor => {
1358 client.state.show_loadout_menu = false;
1359 if client.state.show_rotation_editor {
1360 client.state.show_rotation_editor = false;
1361 client.state.rotation_editor.reset();
1362 } else {
1363 client.state.show_rotation_editor = true;
1364 client.state.rotation_editor.reset();
1365 let max = client.state.rotation_presets.len();
1366 if max > 0 {
1367 client.state.rotation_editor.list_index =
1368 client.state.rotation_editor.list_index.min(max - 1);
1369 }
1370 }
1371 if client.state.show_rotation_editor {
1372 *ctx.auto_nav = None;
1373 let _ = client.stop().await;
1374 }
1375 }
1376 InputAction::LoadoutMenuUp => {
1377 if !client.state.show_loadout_menu {
1378 return;
1379 }
1380 if client.state.loadout_focus_presets {
1381 if client.state.loadout_menu_index > 0 {
1382 client.state.loadout_menu_index -= 1;
1383 }
1384 } else if client.state.loadout_ability_index > 0 {
1385 client.state.loadout_ability_index -= 1;
1386 }
1387 }
1388 InputAction::LoadoutMenuDown => {
1389 if !client.state.show_loadout_menu {
1390 return;
1391 }
1392 if client.state.loadout_focus_presets {
1393 let max = client.state.rotation_presets.len();
1394 if max > 0 {
1395 client.state.loadout_menu_index =
1396 (client.state.loadout_menu_index + 1).min(max - 1);
1397 }
1398 } else {
1399 let max = client.state.loadout_hotbar_choices().len();
1400 if max > 0 {
1401 client.state.loadout_ability_index =
1402 (client.state.loadout_ability_index + 1).min(max - 1);
1403 }
1404 }
1405 }
1406 InputAction::LoadoutHotbarPrev => {
1407 if client.state.show_loadout_menu {
1408 let slot = client.state.loadout_hotbar_slot;
1409 client.state.loadout_hotbar_slot = if slot <= 1 { 9 } else { slot - 1 };
1410 }
1411 }
1412 InputAction::LoadoutHotbarNext => {
1413 if client.state.show_loadout_menu {
1414 let slot = client.state.loadout_hotbar_slot;
1415 client.state.loadout_hotbar_slot = if slot >= 9 { 1 } else { slot + 1 };
1416 }
1417 }
1418 InputAction::LoadoutToggleFocus => {
1419 if client.state.show_loadout_menu {
1420 client.state.loadout_focus_presets = !client.state.loadout_focus_presets;
1421 }
1422 }
1423 InputAction::LoadoutBindHotbar => {
1424 if !client.state.show_loadout_menu {
1425 return;
1426 }
1427 let slot = client.state.loadout_hotbar_slot;
1428 let binding = {
1429 let choices = client.state.loadout_hotbar_choices();
1430 choices
1431 .get(client.state.loadout_ability_index)
1432 .map(|c| c.binding.clone())
1433 };
1434 if let Some(binding) = binding {
1435 if let Err(err) = client.set_hotbar_slot(slot, Some(&binding)).await {
1436 client.state.push_log(format!("Hotbar: {err}"));
1437 }
1438 } else {
1439 client.state.push_log("No ability/consumable selected to bind");
1440 }
1441 }
1442 InputAction::LoadoutClearHotbar => {
1443 if !client.state.show_loadout_menu {
1444 return;
1445 }
1446 let slot = client.state.loadout_hotbar_slot;
1447 if let Err(err) = client.set_hotbar_slot(slot, None).await {
1448 client.state.push_log(format!("Hotbar: {err}"));
1449 }
1450 }
1451 InputAction::LoadoutAssignT1 => {
1452 if !client.state.show_loadout_menu {
1453 return;
1454 }
1455 let preset = {
1456 let idx = client.state.loadout_menu_index;
1457 client.state.rotation_presets.get(idx).cloned()
1458 };
1459 if let Some(preset) = preset {
1460 let already = client
1461 .state
1462 .combat_slots
1463 .iter()
1464 .find(|s| s.slot_index == 1)
1465 .and_then(|s| s.preset_id.as_deref())
1466 == Some(preset.id.as_str());
1467 if already {
1468 client.state.push_log(format!(
1469 "T1 already uses {} (equip weapons from inventory — Weapon auto tracks mainhand)",
1470 preset.label
1471 ));
1472 } else if let Err(err) = client.assign_slot_preset(1, &preset.id).await {
1473 client.state.push_log(format!("Loadout: {err}"));
1474 } else {
1475 client
1476 .state
1477 .push_log(format!("T1 ← {}", preset.label));
1478 }
1479 }
1480 }
1481 InputAction::LoadoutAssignT2 => {
1482 if !client.state.show_loadout_menu {
1483 return;
1484 }
1485 let preset = {
1486 let idx = client.state.loadout_menu_index;
1487 client.state.rotation_presets.get(idx).cloned()
1488 };
1489 if let Some(preset) = preset {
1490 let already = client
1491 .state
1492 .combat_slots
1493 .iter()
1494 .find(|s| s.slot_index == 2)
1495 .and_then(|s| s.preset_id.as_deref())
1496 == Some(preset.id.as_str());
1497 if already {
1498 client
1499 .state
1500 .push_log(format!("T2 already uses {}", preset.label));
1501 } else if let Err(err) = client.assign_slot_preset(2, &preset.id).await {
1502 client.state.push_log(format!("Loadout: {err}"));
1503 } else {
1504 client
1505 .state
1506 .push_log(format!("T2 ← {}", preset.label));
1507 }
1508 }
1509 }
1510 InputAction::CloseOverlay => {
1511 client.state.show_loadout_menu = false;
1512 client.state.show_rotation_editor = false;
1513 client.state.rotation_editor.reset();
1514 }
1515 InputAction::ClearCombatTarget => {
1516 if let Err(err) = client.clear_combat_target().await {
1517 client.state.push_log(format!("Target: {err}"));
1518 }
1519 }
1520 InputAction::ClearCombatTargetT2 => {
1521 if let Err(err) = client.clear_combat_target_slot(2).await {
1522 client.state.push_log(format!("T2 target: {err}"));
1523 }
1524 }
1525 InputAction::Dodge => {
1526 *ctx.auto_nav = None;
1527 if let Err(err) = client.dodge().await {
1528 client.state.push_log(format!("Dodge: {err}"));
1529 }
1530 }
1531 InputAction::Lunge => {
1532 *ctx.auto_nav = None;
1533 let _ = client.stop().await;
1534 if let Err(err) = client.lunge().await {
1535 client.state.push_log(format!("Lunge: {err}"));
1536 }
1537 }
1538 InputAction::DirectionalJump { forward, strafe } => {
1539 *ctx.auto_nav = None;
1540 let _ = client.stop().await;
1541 if let Err(err) = client.directional_jump(forward, strafe).await {
1542 client.state.push_log(format!("Jump: {err}"));
1543 }
1544 }
1545 InputAction::CastHotbar { slot } => {
1546 if let Err(err) = client.cast_hotbar_ability(slot).await {
1547 client.state.push_log(format!("Hotbar {slot}: {err}"));
1548 }
1549 }
1550 InputAction::ToggleBlock => {
1551 if !client.state.blocking_active {
1552 if let Err(err) = client.set_block(true).await {
1553 client.state.push_log(format!("Block: {err}"));
1554 }
1555 }
1556 }
1557 InputAction::ToggleStats => client.toggle_stats(),
1558 InputAction::ToggleEquip => client.toggle_equip_menu(),
1559 InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
1560 InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
1561 InputAction::ToggleInventory => client.toggle_inventory_menu(),
1562 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
1563 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
1564 InputAction::ToggleWorkersMenu => {
1565 if client.state.show_workers_menu {
1566 if let Err(err) = client.close_workers_menu().await {
1567 client.state.push_log(format!("Worker: {err}"));
1568 }
1569 } else {
1570 client.toggle_workers_menu();
1571 }
1572 }
1573 InputAction::QuestMenuUp => client.quest_menu_move(-1),
1574 InputAction::QuestMenuDown => client.quest_menu_move(1),
1575 InputAction::QuestWithdraw => client.quest_request_withdraw(),
1576 InputAction::RotationEditorBack => {
1577 let _ = client.back_on_esc();
1578 }
1579 InputAction::RotationEditorListUp
1580 | InputAction::RotationEditorListDown
1581 | InputAction::RotationEditorEdit
1582 | InputAction::RotationEditorNew
1583 | InputAction::RotationEditorDelete
1584 | InputAction::RotationEditorAddAbility
1585 | InputAction::RotationEditorRemoveAbility
1586 | InputAction::RotationEditorMoveAbilityUp
1587 | InputAction::RotationEditorMoveAbilityDown
1588 | InputAction::RotationEditorAbilityUp
1589 | InputAction::RotationEditorAbilityDown
1590 | InputAction::RotationEditorPickerUp
1591 | InputAction::RotationEditorPickerDown
1592 | InputAction::RotationEditorPickAbility
1593 | InputAction::RotationEditorRename
1594 | InputAction::RotationEditorConfirmLabel
1595 | InputAction::RotationEditorLabelBackspace
1596 | InputAction::RotationEditorLabelChar(_)
1597 | InputAction::RotationEditorSave => {
1598 handle_rotation_editor_action(client, action).await;
1599 }
1600 InputAction::None
1601 | InputAction::Quit
1602 | InputAction::ToggleHelp
1603 | InputAction::CycleHudView
1604 | InputAction::SubmitChat
1605 | InputAction::CancelChat
1606 | InputAction::ToggleSprintMode
1607 | InputAction::ToggleMapTarget
1608 | InputAction::ConfirmMapTarget
1609 | InputAction::CancelMapTarget
1610 | InputAction::MapTargetNudge { .. }
1611 | InputAction::CancelAutoNav
1612 | InputAction::StopMovement => {}
1613 InputAction::ToggleHudLog => {
1614 let hud_view = flatland_client_lib::ClientConfig::load()
1615 .hud_view
1616 .as_deref()
1617 .and_then(flatland_client_ui::HudViewMode::from_label)
1618 .unwrap_or_default();
1619 if hud_view != flatland_client_ui::HudViewMode::Normal {
1620 client.state.push_log("LOG hide/show only works in normal HUD (press .)");
1621 return;
1622 }
1623 client.state.hud_log_hidden = !client.state.hud_log_hidden;
1624 let mut cfg = flatland_client_lib::ClientConfig::load();
1625 let _ = cfg.save_hud_log_hidden(client.state.hud_log_hidden);
1626 if client.state.hud_log_hidden {
1627 client.state.push_log("LOG hidden — press ' to show (normal HUD)");
1628 } else {
1629 client.state.push_log("LOG shown — press ' to hide");
1630 }
1631 }
1632 }
1633}
1634
1635async fn handle_rotation_editor_action<S: PlayConnection>(
1636 client: &mut GameClient<S>,
1637 action: InputAction,
1638) {
1639 match action {
1640 InputAction::RotationEditorListUp => {
1641 if client.state.rotation_editor.list_index > 0 {
1642 client.state.rotation_editor.list_index -= 1;
1643 }
1644 }
1645 InputAction::RotationEditorListDown => {
1646 let max = client.state.rotation_presets.len();
1647 if max > 0 {
1648 client.state.rotation_editor.list_index =
1649 (client.state.rotation_editor.list_index + 1).min(max - 1);
1650 }
1651 }
1652 InputAction::RotationEditorEdit => {
1653 let idx = client.state.rotation_editor.list_index;
1654 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
1655 client.state.rotation_editor.draft = Some(preset);
1656 client.state.rotation_editor.ability_index = 0;
1657 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1658 }
1659 }
1660 InputAction::RotationEditorNew => {
1661 let preset = next_custom_preset(&client.state.rotation_presets);
1662 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
1663 client.state.rotation_editor.draft = Some(preset);
1664 client.state.rotation_editor.ability_index = 0;
1665 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1666 }
1667 InputAction::RotationEditorDelete => {
1668 let idx = client.state.rotation_editor.list_index;
1669 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
1670 if let Some(id) = preset_id {
1671 if preset_deletable(&id) {
1672 if let Err(err) = client.delete_rotation_preset(&id).await {
1673 client.state.push_log(format!("Delete: {err}"));
1674 } else {
1675 let max = client.state.rotation_presets.len();
1676 client.state.rotation_editor.list_index = if max == 0 {
1677 0
1678 } else {
1679 client.state.rotation_editor.list_index.min(max - 1)
1680 };
1681 }
1682 } else {
1683 client.state.push_log("Cannot delete built-in preset");
1684 }
1685 }
1686 }
1687 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
1688 RotationEditorMode::EditLabel => {
1689 client.state.rotation_editor.label_buffer.clear();
1690 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1691 }
1692 RotationEditorMode::PickAbility => {
1693 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1694 }
1695 RotationEditorMode::EditSequence => {
1696 client.state.rotation_editor.draft = None;
1697 client.state.rotation_editor.mode = RotationEditorMode::List;
1698 }
1699 RotationEditorMode::List => {}
1700 },
1701 InputAction::RotationEditorAddAbility => {
1702 client.state.rotation_editor.picker_index = 0;
1703 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
1704 }
1705 InputAction::RotationEditorRemoveAbility => {
1706 let idx = client.state.rotation_editor.ability_index;
1707 if let Some(draft) = &mut client.state.rotation_editor.draft {
1708 if idx < draft.abilities.len() {
1709 draft.abilities.remove(idx);
1710 if client.state.rotation_editor.ability_index >= draft.abilities.len()
1711 && client.state.rotation_editor.ability_index > 0
1712 {
1713 client.state.rotation_editor.ability_index -= 1;
1714 }
1715 }
1716 }
1717 }
1718 InputAction::RotationEditorMoveAbilityUp => {
1719 let i = client.state.rotation_editor.ability_index;
1720 if let Some(draft) = &mut client.state.rotation_editor.draft {
1721 if i > 0 && i < draft.abilities.len() {
1722 draft.abilities.swap(i, i - 1);
1723 client.state.rotation_editor.ability_index -= 1;
1724 }
1725 }
1726 }
1727 InputAction::RotationEditorMoveAbilityDown => {
1728 let i = client.state.rotation_editor.ability_index;
1729 if let Some(draft) = &mut client.state.rotation_editor.draft {
1730 if i + 1 < draft.abilities.len() {
1731 draft.abilities.swap(i, i + 1);
1732 client.state.rotation_editor.ability_index += 1;
1733 }
1734 }
1735 }
1736 InputAction::RotationEditorAbilityUp => {
1737 if client.state.rotation_editor.ability_index > 0 {
1738 client.state.rotation_editor.ability_index -= 1;
1739 }
1740 }
1741 InputAction::RotationEditorAbilityDown => {
1742 if let Some(draft) = &client.state.rotation_editor.draft {
1743 if !draft.abilities.is_empty() {
1744 client.state.rotation_editor.ability_index =
1745 (client.state.rotation_editor.ability_index + 1)
1746 .min(draft.abilities.len() - 1);
1747 }
1748 }
1749 }
1750 InputAction::RotationEditorPickerUp => {
1751 if client.state.rotation_editor.picker_index > 0 {
1752 client.state.rotation_editor.picker_index -= 1;
1753 }
1754 }
1755 InputAction::RotationEditorPickerDown => {
1756 let choices = editor_ability_choices(
1757 &client.state.known_abilities,
1758 &client.state.weapon_ability_id,
1759 );
1760 if !choices.is_empty() {
1761 client.state.rotation_editor.picker_index =
1762 (client.state.rotation_editor.picker_index + 1).min(choices.len() - 1);
1763 }
1764 }
1765 InputAction::RotationEditorPickAbility => {
1766 let choices = editor_ability_choices(
1767 &client.state.known_abilities,
1768 &client.state.weapon_ability_id,
1769 );
1770 let pick = client.state.rotation_editor.picker_index;
1771 if let Some(ability) = choices.get(pick) {
1772 if let Some(draft) = &mut client.state.rotation_editor.draft {
1773 let max = client.state.max_abilities_per_rotation.max(1) as usize;
1774 if draft.abilities.len() >= max {
1775 client
1776 .state
1777 .push_log(format!("Rotation full (max {max} from INT+WIS)"));
1778 } else {
1779 draft.abilities.push(ability.clone());
1780 client.state.rotation_editor.ability_index =
1781 draft.abilities.len().saturating_sub(1);
1782 }
1783 }
1784 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1785 }
1786 }
1787 InputAction::RotationEditorRename => {
1788 if let Some(draft) = &client.state.rotation_editor.draft {
1789 client.state.rotation_editor.label_buffer = draft.label.clone();
1790 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
1791 }
1792 }
1793 InputAction::RotationEditorConfirmLabel => {
1794 let label = client.state.rotation_editor.label_buffer.trim().to_string();
1795 if label.is_empty() {
1796 client.state.push_log("Label cannot be empty");
1797 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1798 draft.label = label;
1799 client.state.rotation_editor.label_buffer.clear();
1800 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1801 }
1802 }
1803 InputAction::RotationEditorLabelBackspace => {
1804 client.state.rotation_editor.label_buffer.pop();
1805 }
1806 InputAction::RotationEditorLabelChar(c) => {
1807 if client.state.rotation_editor.label_buffer.len() < 32 {
1808 client.state.rotation_editor.label_buffer.push(c);
1809 }
1810 }
1811 InputAction::RotationEditorSave => {
1812 let draft = match client.state.rotation_editor.draft.clone() {
1813 Some(d) => d,
1814 None => return,
1815 };
1816 if draft.abilities.is_empty() {
1817 client.state.push_log("Rotation needs at least one ability");
1818 return;
1819 }
1820 let saved_id = draft.id.clone();
1821 if let Err(err) = client.upsert_rotation_preset(draft).await {
1822 client.state.push_log(format!("Save: {err}"));
1823 } else {
1824 client.state.rotation_editor.mode = RotationEditorMode::List;
1825 client.state.rotation_editor.draft = None;
1826 if let Some(i) = client
1827 .state
1828 .rotation_presets
1829 .iter()
1830 .position(|p| p.id == saved_id)
1831 {
1832 client.state.rotation_editor.list_index = i;
1833 }
1834 }
1835 }
1836 _ => {}
1837 }
1838}