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