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