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.show_worker_rename {
26 match key.code {
27 UiKeyCode::Esc => client.cancel_worker_rename(),
28 UiKeyCode::Enter => {
29 if let Err(err) = client.confirm_worker_rename().await {
30 client.state.push_log(format!("Rename: {err}"));
31 }
32 }
33 UiKeyCode::Backspace => {
34 client.state.rename_buffer.pop();
35 }
36 UiKeyCode::Char(c) => {
37 if client.state.rename_buffer.chars().count() < 32 {
38 client.state.rename_buffer.push(c);
39 }
40 }
41 _ => {}
42 }
43 return true;
44 }
45
46 if client.state.show_equip_menu {
47 match key.code {
48 UiKeyCode::Char('p') | UiKeyCode::Esc => {
49 client.toggle_equip_menu();
50 }
51 UiKeyCode::Up | UiKeyCode::Char('k') => {
52 if client.state.equip_menu_index > 0 {
53 client.state.equip_menu_index -= 1;
54 }
55 }
56 UiKeyCode::Down | UiKeyCode::Char('j') => {
57 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
58 if n > 0 {
59 client.state.equip_menu_index = (client.state.equip_menu_index + 1).min(n - 1);
60 }
61 }
62 UiKeyCode::PageUp => {
63 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
64 client.state.equip_menu_index =
65 flatland_client_lib::page_list_index(client.state.equip_menu_index, -1, n);
66 }
67 UiKeyCode::PageDown => {
68 let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
69 client.state.equip_menu_index =
70 flatland_client_lib::page_list_index(client.state.equip_menu_index, 1, n);
71 }
72 UiKeyCode::Enter => {
73 if let Err(err) = client.activate_equip_selection().await {
74 client.state.push_log(format!("Equip: {err}"));
75 }
76 }
77 _ => {}
78 }
79 return true;
80 }
81
82 if client.state.show_inventory_menu {
83 if client.state.show_rename_prompt {
84 match key.code {
85 UiKeyCode::Esc => client.cancel_rename_prompt(),
86 UiKeyCode::Enter => {
87 if let Err(err) = client.confirm_rename_prompt().await {
88 client.state.push_log(format!("Rename: {err}"));
89 }
90 }
91 UiKeyCode::Backspace => {
92 client.state.rename_buffer.pop();
93 }
94 UiKeyCode::Char(c) => {
95 if client.state.rename_buffer.len() < 32 {
96 client.state.rename_buffer.push(c);
97 }
98 }
99 _ => {}
100 }
101 } else if client.state.show_destroy_picker {
102 match key.code {
103 UiKeyCode::Esc => {
104 if client.state.destroy_confirm_pending {
105 client.cancel_destroy_confirm();
106 } else {
107 client.close_destroy_picker();
108 }
109 }
110 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
111 if !client.state.destroy_confirm_pending {
112 client.destroy_picker_adjust_quantity(-1);
113 }
114 }
115 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
116 if !client.state.destroy_confirm_pending {
117 client.destroy_picker_adjust_quantity(1);
118 }
119 }
120 UiKeyCode::Char('a') => {
121 if !client.state.destroy_confirm_pending {
122 client.destroy_picker_set_quantity_max();
123 }
124 }
125 UiKeyCode::Enter => {
126 if let Err(err) = client.activate_inventory_selection().await {
127 client.state.push_log(format!("Destroy failed: {err}"));
128 }
129 }
130 _ => {}
131 }
132 } else if client.state.show_grant_picker {
133 let filter_focused = client
134 .state
135 .grant_picker
136 .as_ref()
137 .map(|p| p.filter_focused)
138 .unwrap_or(false);
139 match key.code {
140 UiKeyCode::Esc => {
141 if !client.clear_or_blur_inventory_filter() {
142 client.close_grant_picker();
143 }
144 }
145 UiKeyCode::PageUp => client.inventory_menu_page(-1),
146 UiKeyCode::PageDown => client.inventory_menu_page(1),
147 UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
148 UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
149 UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
150 UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
151 client.inventory_menu_move(-1)
152 }
153 UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
154 client.inventory_menu_move(1)
155 }
156 UiKeyCode::Enter if !filter_focused => {
157 if let Err(err) = client.activate_inventory_selection().await {
158 client.state.push_log(format!("Grant failed: {err}"));
159 }
160 }
161 _ => {}
162 }
163 } else if client.state.show_move_picker {
164 let filter_focused = client
165 .state
166 .move_picker
167 .as_ref()
168 .map(|p| p.filter_focused)
169 .unwrap_or(false);
170 match key.code {
171 UiKeyCode::Esc => {
172 if !client.clear_or_blur_inventory_filter() {
173 client.close_move_picker();
174 }
175 }
176 UiKeyCode::PageUp => client.inventory_menu_page(-1),
177 UiKeyCode::PageDown => client.inventory_menu_page(1),
178 UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
179 UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
180 UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
181 UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
182 client.inventory_menu_move(-1)
183 }
184 UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
185 client.inventory_menu_move(1)
186 }
187 UiKeyCode::Char('[') | UiKeyCode::Char('-') if !filter_focused => {
188 client.move_picker_adjust_quantity(-1);
189 }
190 UiKeyCode::Char(']') | UiKeyCode::Char('=') if !filter_focused => {
191 client.move_picker_adjust_quantity(1);
192 }
193 UiKeyCode::Char('a') if !filter_focused => client.move_picker_set_quantity_max(),
194 UiKeyCode::Enter if !filter_focused => {
195 if let Err(err) = client.activate_inventory_selection().await {
196 client.state.push_log(format!("Move failed: {err}"));
197 }
198 }
199 _ => {}
200 }
201 } else if client.state.inventory_filter_focused {
202 match key.code {
203 UiKeyCode::Esc => {
204 let _ = client.clear_or_blur_inventory_filter();
205 }
206 UiKeyCode::Enter => {
207 client.state.inventory_filter_focused = false;
208 }
209 UiKeyCode::Backspace => client.inventory_filter_backspace(),
210 UiKeyCode::Char(c) => client.append_inventory_filter_char(c),
211 _ => {}
212 }
213 } else {
214 match key.code {
215 UiKeyCode::Esc => {
216 if !client.clear_or_blur_inventory_filter() {
217 client.close_inventory_menu();
218 }
219 }
220 UiKeyCode::Char('b') => client.close_inventory_menu(),
221 UiKeyCode::Tab => client.cycle_inventory_tab(true),
222 UiKeyCode::BackTab => client.cycle_inventory_tab(false),
223 UiKeyCode::PageUp => client.inventory_menu_page(-1),
224 UiKeyCode::PageDown => client.inventory_menu_page(1),
225 UiKeyCode::Char('/') => client.focus_inventory_filter(),
226 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
227 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
228 UiKeyCode::Enter => {
229 if let Err(err) = client.activate_inventory_selection().await {
230 client.state.push_log(format!("{err}"));
231 }
232 }
233 UiKeyCode::Char('m') => {
234 if let Err(err) = client.open_move_picker() {
235 client.state.push_log(format!("{err}"));
236 }
237 }
238 UiKeyCode::Char('e') => {
239 if let Err(err) = client.use_selected_consumable().await {
240 client.state.push_log(format!("Use: {err}"));
241 }
242 }
243 UiKeyCode::Char('n') => {
244 if let Err(err) = client.open_rename_prompt() {
245 client.state.push_log(format!("{err}"));
246 }
247 }
248 UiKeyCode::Char('d') => {
249 if let Err(err) = client.drop_selected().await {
250 client.state.push_log(format!("Drop: {err}"));
251 }
252 }
253 UiKeyCode::Char('x') => {
254 if let Err(err) = client.open_destroy_picker() {
255 client.state.push_log(format!("Destroy: {err}"));
256 }
257 }
258 UiKeyCode::Char('l') => {
259 if let Err(err) = client.toggle_chest_lock_for_selection().await {
260 client.state.push_log(format!("Lock: {err}"));
261 }
262 }
263 UiKeyCode::Char('g') => {
264 if let Err(err) = client.give_selected_inventory_to_worker().await {
265 client.state.push_log(format!("Give: {err}"));
266 }
267 }
268 UiKeyCode::Char('u') => {
269 if let Err(err) = client.unequip_mainhand().await {
270 client.state.push_log(format!("Unequip: {err}"));
271 }
272 }
273 _ => {}
274 }
275 }
276 return true;
277 }
278
279 if client.state.show_keychain_menu {
280 match key.code {
281 UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
282 UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
283 client.keychain_menu_move(-1);
284 }
285 UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
286 client.keychain_menu_move(1);
287 }
288 UiKeyCode::PageUp => client.keychain_menu_page(-1),
289 UiKeyCode::PageDown => client.keychain_menu_page(1),
290 UiKeyCode::Enter => {
291 if let Err(err) = client.activate_keychain_selection().await {
292 client.state.push_log(format!("Keychain: {err}"));
293 }
294 }
295 _ => {}
296 }
297 return true;
298 }
299
300 if client.state.show_craft_menu {
301 match key.code {
302 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
303 client.close_craft_menu()
304 }
305 UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
306 UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
307 UiKeyCode::PageUp => client.craft_menu_page(-1),
308 UiKeyCode::PageDown => client.craft_menu_page(1),
309 UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
310 UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
311 UiKeyCode::Char('a') => client.craft_batch_set_max(),
312 UiKeyCode::Enter => {
313 if let Err(err) = client.craft_menu_selection().await {
314 client.state.push_log(format!("Craft failed: {err}"));
315 }
316 }
317 _ => {}
318 }
319 return true;
320 }
321
322 if client.state.show_quest_offer {
323 match key.code {
324 UiKeyCode::Esc => client.quest_offer_decline(),
325 UiKeyCode::Enter => {
326 if let Err(err) = client.quest_offer_accept().await {
327 client.state.push_log(format!("Quest: {err}"));
328 }
329 }
330 _ => {}
331 }
332 return true;
333 }
334
335 if client.state.show_npc_chat {
336 match key.code {
337 UiKeyCode::Esc => {
338 if let Err(err) = client.npc_talk_close().await {
339 client.state.push_log(format!("Talk close failed: {err}"));
340 }
341 }
342 UiKeyCode::Enter => {
343 if let Err(err) = client.npc_talk_send().await {
344 client.state.push_log(format!("Talk failed: {err}"));
345 }
346 }
347 UiKeyCode::Backspace => {
348 if let Some(chat) = client.state.npc_chat.as_mut() {
349 chat.input.pop();
350 }
351 }
352 UiKeyCode::Char(c) => {
353 if let Some(chat) = client.state.npc_chat.as_mut() {
354 if !chat.pending && chat.input.len() < 300 {
355 chat.input.push(c);
356 }
357 }
358 }
359 _ => {}
360 }
361 return true;
362 }
363
364 if client.state.show_npc_verb_menu {
365 match key.code {
366 UiKeyCode::Esc => {
367 client.state.show_npc_verb_menu = false;
368 client.state.npc_verb_target = None;
369 }
370 UiKeyCode::Up | UiKeyCode::Char('k') => {
371 if client.state.npc_verb_index > 0 {
372 client.state.npc_verb_index -= 1;
373 }
374 }
375 UiKeyCode::Down | UiKeyCode::Char('j') => {
376 let max = client.npc_verb_options().len().saturating_sub(1);
377 if client.state.npc_verb_index < max {
378 client.state.npc_verb_index += 1;
379 }
380 }
381 UiKeyCode::Enter => {
382 if let Err(err) = client.confirm_npc_verb().await {
383 client.state.push_log(format!("Interact failed: {err}"));
384 }
385 }
386 _ => {}
387 }
388 return true;
389 }
390
391 if client.state.bank_panel.is_some() {
392 match &client.state.bank_ui_mode {
393 flatland_client_lib::BankUiMode::Menu => match key.code {
394 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
395 if let Err(err) = client.close_bank_panel().await {
396 client.state.push_log(format!("Bank close failed: {err}"));
397 }
398 }
399 UiKeyCode::Up | UiKeyCode::Char('k') => client.bank_menu_move(-1),
400 UiKeyCode::Down | UiKeyCode::Char('j') => client.bank_menu_move(1),
401 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
402 if let Err(err) = client.confirm_bank_menu().await {
403 client.state.push_log(format!("Bank action failed: {err}"));
404 }
405 }
406 UiKeyCode::Char('d') => {
407 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::DepositAmount {
408 input: String::new(),
409 };
410 }
411 UiKeyCode::Char('w') => {
412 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::WithdrawAmount {
413 input: String::new(),
414 };
415 }
416 UiKeyCode::Char('t') => {
417 client.state.bank_ui_mode = flatland_client_lib::BankUiMode::TransferName {
418 input: String::new(),
419 };
420 }
421 _ => {}
422 },
423 _ => match key.code {
424 UiKeyCode::Esc | UiKeyCode::Char('[') => client.bank_transfer_back(),
425 UiKeyCode::Enter => {
426 if let Err(err) = client.confirm_bank_menu().await {
427 client.state.push_log(format!("Bank action failed: {err}"));
428 }
429 }
430 UiKeyCode::Backspace => client.bank_transfer_backspace(),
431 UiKeyCode::Char(c) => client.bank_transfer_append_char(c),
432 _ => {}
433 },
434 }
435 return true;
436 }
437
438 if client.state.storage_panel.is_some() {
439 match &client.state.storage_ui_mode {
440 flatland_client_lib::StorageUiMode::Menu => match key.code {
441 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
442 if let Err(err) = client.close_storage_panel().await {
443 client.state.push_log(format!("Storage close failed: {err}"));
444 }
445 }
446 UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_menu_move(-1),
447 UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_menu_move(1),
448 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
449 if let Err(err) = client.confirm_storage_menu().await {
450 client.state.push_log(format!("Storage action failed: {err}"));
451 }
452 }
453 UiKeyCode::Char('s') => {
454 let opts = client.state.storage_store_options();
455 if opts.is_empty() {
456 client.state.push_log("Nothing loose to store.");
457 } else {
458 client.state.storage_ui_mode =
459 flatland_client_lib::StorageUiMode::StorePick { index: 0 };
460 }
461 }
462 UiKeyCode::Char('t') => {
463 let opts = client.state.storage_vault_options();
464 if opts.is_empty() {
465 client.state.push_log("Vault is empty.");
466 } else {
467 client.state.storage_ui_mode =
468 flatland_client_lib::StorageUiMode::TakePick { index: 0 };
469 }
470 }
471 _ => {}
472 },
473 flatland_client_lib::StorageUiMode::StoreAmount { .. }
474 | flatland_client_lib::StorageUiMode::TakeAmount { .. }
475 | flatland_client_lib::StorageUiMode::ShipAmount { .. } => match key.code {
476 UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
477 UiKeyCode::Enter => {
478 if let Err(err) = client.confirm_storage_menu().await {
479 client.state.push_log(format!("Storage action failed: {err}"));
480 }
481 }
482 UiKeyCode::Backspace => client.storage_amount_backspace(),
483 UiKeyCode::Char(c) => client.storage_amount_append_char(c),
484 _ => {}
485 },
486 _ => match key.code {
487 UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
488 UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_pick_move(-1),
489 UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_pick_move(1),
490 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
491 if let Err(err) = client.confirm_storage_menu().await {
492 client.state.push_log(format!("Storage action failed: {err}"));
493 }
494 }
495 _ => {}
496 },
497 }
498 return true;
499 }
500
501 if client.state.show_shop_menu {
502 match key.code {
503 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
504 if let Err(err) = client.back_from_shop_menu().await {
505 client.state.push_log(format!("Shop close failed: {err}"));
506 }
507 }
508 UiKeyCode::Tab => client.shop_tab_toggle(),
509 UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
510 UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
511 UiKeyCode::PageUp => client.shop_menu_page(-1),
512 UiKeyCode::PageDown => client.shop_menu_page(1),
513 UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
514 UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
515 UiKeyCode::Char('a') => client.shop_quantity_set_max(),
516 UiKeyCode::Enter => {
517 if let Err(err) = client.shop_confirm().await {
518 client.state.push_log(format!("Trade failed: {err}"));
519 }
520 }
521 _ => {}
522 }
523 return true;
524 }
525
526 if client.state.show_quest_menu {
527 match key.code {
528 UiKeyCode::Esc => {
529 if client.state.quest_withdraw_confirm {
530 client.state.quest_withdraw_confirm = false;
531 } else {
532 client.state.show_quest_menu = false;
533 }
534 }
535 UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
536 UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
537 UiKeyCode::PageUp => client.quest_menu_page(-1),
538 UiKeyCode::PageDown => client.quest_menu_page(1),
539 UiKeyCode::Char('x') => client.quest_request_withdraw(),
540 UiKeyCode::Enter => {
541 if let Err(err) = client.quest_confirm_action().await {
542 client.state.push_log(format!("Quest: {err}"));
543 }
544 }
545 _ => {}
546 }
547 return true;
548 }
549
550 if client.state.worker_route_editor.is_some() {
551 let at_root = client.re_at_root_sheet();
552 let sheet_index = client.re_sheet_index();
553 if at_root {
554 match key.code {
556 UiKeyCode::Esc => client.close_worker_route_editor(),
557 UiKeyCode::Char('s') => {
558 if let Err(err) = client.worker_route_editor_save().await {
559 client.state.push_log(format!("Route: {err}"));
560 }
561 }
562 UiKeyCode::Down => {
563 if key.modifiers.contains_control() {
564 client.worker_route_editor_move_selected(1);
565 } else {
566 client.worker_route_editor_select(1);
567 }
568 }
569 UiKeyCode::Up => {
570 if key.modifiers.contains_control() {
571 client.worker_route_editor_move_selected(-1);
572 } else {
573 client.worker_route_editor_select(-1);
574 }
575 }
576 UiKeyCode::Char('j') => {
579 if key.modifiers.contains_control() {
580 client.worker_route_editor_move_selected(-1);
581 } else {
582 client.worker_route_editor_select(1);
583 }
584 }
585 UiKeyCode::Char('k') => {
586 if key.modifiers.contains_control() {
587 client.worker_route_editor_move_selected(1);
588 } else {
589 client.worker_route_editor_select(-1);
590 }
591 }
592 UiKeyCode::Char('d') | UiKeyCode::Delete => {
593 client.worker_route_editor_delete_selected()
594 }
595 UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
596 UiKeyCode::Char('a') => client.re_open_add_menu(),
597 UiKeyCode::Char('l') => client.re_open_bed_picker(),
598 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
599 UiKeyCode::Enter => client.re_edit_selected_stop(),
600 _ => {}
601 }
602 } else {
603 match key.code {
605 UiKeyCode::Esc => client.re_sheet_back(),
606 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
607 UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
608 UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
609 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
610 client.re_sheet_row_activate(sheet_index)
611 }
612 UiKeyCode::Char('[') | UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
613 UiKeyCode::Char(']') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
614 _ => {}
615 }
616 }
617 return true;
618 }
619
620 if client.state.show_worker_give_picker {
621 match key.code {
622 UiKeyCode::Esc => client.close_worker_give_picker(),
623 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
624 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
625 UiKeyCode::Enter => {
626 if let Err(err) = client.confirm_worker_give_picker().await {
627 client.state.push_log(format!("Give: {err}"));
628 }
629 }
630 _ => {}
631 }
632 return true;
633 }
634
635 if client.state.show_worker_give_target_picker {
636 match key.code {
637 UiKeyCode::Esc => client.close_worker_give_target_picker(),
638 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_target_picker_move(-1),
639 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_target_picker_move(1),
640 UiKeyCode::Enter => {
641 if let Err(err) = client.confirm_worker_give_target_picker().await {
642 client.state.push_log(format!("Give: {err}"));
643 }
644 }
645 _ => {}
646 }
647 return true;
648 }
649
650 if client.state.show_worker_take_picker {
651 match key.code {
652 UiKeyCode::Esc => client.close_worker_take_picker(),
653 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_take_picker_move(-1),
654 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_take_picker_move(1),
655 UiKeyCode::Enter => {
656 if let Err(err) = client.confirm_worker_take_picker().await {
657 client.state.push_log(format!("Take: {err}"));
658 }
659 }
660 _ => {}
661 }
662 return true;
663 }
664
665 if client.state.show_worker_teach_picker {
666 match key.code {
667 UiKeyCode::Esc => client.close_worker_teach_picker(),
668 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
669 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
670 UiKeyCode::Enter => {
671 if let Err(err) = client.confirm_worker_teach_picker().await {
672 client.state.push_log(format!("Teach: {err}"));
673 }
674 }
675 _ => {}
676 }
677 return true;
678 }
679
680 if client.state.show_workers_menu {
681 match key.code {
682 UiKeyCode::Esc => client.state.show_workers_menu = false,
683 UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
684 UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
685 UiKeyCode::PageUp => client.workers_menu_page(-1),
686 UiKeyCode::PageDown => client.workers_menu_page(1),
687 UiKeyCode::Char('d') => {
688 if let Err(err) = client.workers_dismiss_selected().await {
689 client.state.push_log(format!("Worker: {err}"));
690 }
691 }
692 UiKeyCode::Char('r') => {
693 if let Err(err) = client.hire_worker_laborer().await {
694 client.state.push_log(format!("Hire: {err}"));
695 }
696 }
697 UiKeyCode::Char('e') => {
698 if let Err(err) = client.open_worker_route_editor_for_selected() {
699 client.state.push_log(format!("Route: {err}"));
700 }
701 }
702 UiKeyCode::Char('n') => {
703 if let Err(err) = client.open_worker_rename() {
704 client.state.push_log(format!("Rename: {err}"));
705 }
706 }
707 UiKeyCode::Char('g') => {
708 if let Err(err) = client.open_worker_give_picker() {
709 client.state.push_log(format!("Give: {err}"));
710 }
711 }
712 UiKeyCode::Char('i') => {
713 if let Err(err) = client.open_worker_take_picker() {
714 client.state.push_log(format!("Take: {err}"));
715 }
716 }
717 UiKeyCode::Char('t') => {
718 if let Err(err) = client.open_worker_teach_picker() {
719 client.state.push_log(format!("Teach: {err}"));
720 }
721 }
722 UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
723 UiKeyCode::Enter => {
724 if let Err(err) = client.workers_confirm_action().await {
725 client.state.push_log(format!("Worker: {err}"));
726 }
727 }
728 _ => {}
729 }
730 return true;
731 }
732
733 false
734}
735
736pub async fn dispatch_action<S: PlayConnection>(
737 client: &mut GameClient<S>,
738 _keys: &ClientKeyBindings,
739 action: InputAction,
740 ctx: &mut ActionCtx<'_>,
741) {
742 match action {
743 InputAction::Harvest => {
744 if let Err(err) = client.harvest_nearest().await {
745 client.state.push_log(format!("Harvest failed: {err}"));
746 }
747 }
748 InputAction::Pickup => {
749 if client.pickup_nearest().await.is_err() {
750 if let Err(err) = client.pickup_nearest_container().await {
751 client.state.push_log(format!("Pickup: {err}"));
752 }
753 }
754 }
755 InputAction::Craft => client.open_craft_menu(),
756 InputAction::Interact | InputAction::UseWorld => {
757 if let Err(err) = client.use_nearest().await {
758 client.state.push_log(format!("Use: {err}"));
759 }
760 }
761 InputAction::TestDamage => {
762 if let Err(err) = client.test_damage(25.0).await {
763 client.state.push_log(format!("Damage failed: {err}"));
764 }
765 }
766 InputAction::CycleCombatTarget { reverse } => {
767 if let Err(err) = client.cycle_combat_target(reverse).await {
768 client.state.push_log(format!("T1 target: {err}"));
769 }
770 }
771 InputAction::CycleCombatTargetT2 { reverse } => {
772 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
773 client.state.push_log(format!("T2 target: {err}"));
774 }
775 }
776 InputAction::AdvanceRotationT1 => {
777 if let Err(err) = client.advance_rotation(1).await {
778 client.state.push_log(format!("T1 step: {err}"));
779 }
780 }
781 InputAction::AdvanceRotationT2 => {
782 if let Err(err) = client.advance_rotation(2).await {
783 client.state.push_log(format!("T2 step: {err}"));
784 }
785 }
786 InputAction::ToggleAutoT1 => {
787 if let Err(err) = client.toggle_auto_attack_slot(1).await {
788 client.state.push_log(format!("T1 auto: {err}"));
789 }
790 }
791 InputAction::ToggleAutoT2 => {
792 if let Err(err) = client.toggle_auto_attack_slot(2).await {
793 client.state.push_log(format!("T2 auto: {err}"));
794 }
795 }
796 InputAction::ToggleLoadout => {
797 client.state.show_rotation_editor = false;
798 client.state.rotation_editor.reset();
799 client.state.show_loadout_menu = !client.state.show_loadout_menu;
800 if client.state.show_loadout_menu {
801 client.state.loadout_focus_presets = true;
802 client.state.loadout_hotbar_slot = client.state.loadout_hotbar_slot.clamp(1, 9);
803 let ability_len = client.state.loadout_ability_choices().len();
804 if ability_len > 0 {
805 client.state.loadout_ability_index =
806 client.state.loadout_ability_index.min(ability_len - 1);
807 } else {
808 client.state.loadout_ability_index = 0;
809 }
810 let preset_len = client.state.rotation_presets.len();
811 if preset_len > 0 {
812 client.state.loadout_menu_index =
813 client.state.loadout_menu_index.min(preset_len - 1);
814 } else {
815 client.state.loadout_menu_index = 0;
816 }
817 *ctx.auto_nav = None;
818 let _ = client.stop().await;
819 }
820 }
821 InputAction::ToggleRotationEditor => {
822 client.state.show_loadout_menu = false;
823 if client.state.show_rotation_editor {
824 client.state.show_rotation_editor = false;
825 client.state.rotation_editor.reset();
826 } else {
827 client.state.show_rotation_editor = true;
828 client.state.rotation_editor.reset();
829 let max = client.state.rotation_presets.len();
830 if max > 0 {
831 client.state.rotation_editor.list_index =
832 client.state.rotation_editor.list_index.min(max - 1);
833 }
834 }
835 if client.state.show_rotation_editor {
836 *ctx.auto_nav = None;
837 let _ = client.stop().await;
838 }
839 }
840 InputAction::LoadoutMenuUp => {
841 if !client.state.show_loadout_menu {
842 return;
843 }
844 if client.state.loadout_focus_presets {
845 if client.state.loadout_menu_index > 0 {
846 client.state.loadout_menu_index -= 1;
847 }
848 } else if client.state.loadout_ability_index > 0 {
849 client.state.loadout_ability_index -= 1;
850 }
851 }
852 InputAction::LoadoutMenuDown => {
853 if !client.state.show_loadout_menu {
854 return;
855 }
856 if client.state.loadout_focus_presets {
857 let max = client.state.rotation_presets.len();
858 if max > 0 {
859 client.state.loadout_menu_index =
860 (client.state.loadout_menu_index + 1).min(max - 1);
861 }
862 } else {
863 let max = client.state.loadout_ability_choices().len();
864 if max > 0 {
865 client.state.loadout_ability_index =
866 (client.state.loadout_ability_index + 1).min(max - 1);
867 }
868 }
869 }
870 InputAction::LoadoutHotbarPrev => {
871 if client.state.show_loadout_menu {
872 let slot = client.state.loadout_hotbar_slot;
873 client.state.loadout_hotbar_slot = if slot <= 1 { 9 } else { slot - 1 };
874 }
875 }
876 InputAction::LoadoutHotbarNext => {
877 if client.state.show_loadout_menu {
878 let slot = client.state.loadout_hotbar_slot;
879 client.state.loadout_hotbar_slot = if slot >= 9 { 1 } else { slot + 1 };
880 }
881 }
882 InputAction::LoadoutToggleFocus => {
883 if client.state.show_loadout_menu {
884 client.state.loadout_focus_presets = !client.state.loadout_focus_presets;
885 }
886 }
887 InputAction::LoadoutBindHotbar => {
888 if !client.state.show_loadout_menu {
889 return;
890 }
891 let slot = client.state.loadout_hotbar_slot;
892 let ability = {
893 let choices = client.state.loadout_ability_choices();
894 choices
895 .get(client.state.loadout_ability_index)
896 .cloned()
897 };
898 if let Some(ability_id) = ability {
899 if let Err(err) = client.set_hotbar_slot(slot, Some(&ability_id)).await {
900 client.state.push_log(format!("Hotbar: {err}"));
901 }
902 } else {
903 client.state.push_log("No ability selected to bind");
904 }
905 }
906 InputAction::LoadoutClearHotbar => {
907 if !client.state.show_loadout_menu {
908 return;
909 }
910 let slot = client.state.loadout_hotbar_slot;
911 if let Err(err) = client.set_hotbar_slot(slot, None).await {
912 client.state.push_log(format!("Hotbar: {err}"));
913 }
914 }
915 InputAction::LoadoutAssignT1 => {
916 if !client.state.show_loadout_menu {
917 return;
918 }
919 let preset = {
920 let idx = client.state.loadout_menu_index;
921 client.state.rotation_presets.get(idx).cloned()
922 };
923 if let Some(preset) = preset {
924 let already = client
925 .state
926 .combat_slots
927 .iter()
928 .find(|s| s.slot_index == 1)
929 .and_then(|s| s.preset_id.as_deref())
930 == Some(preset.id.as_str());
931 if already {
932 client.state.push_log(format!(
933 "T1 already uses {} (equip weapons from inventory — Weapon auto tracks mainhand)",
934 preset.label
935 ));
936 } else if let Err(err) = client.assign_slot_preset(1, &preset.id).await {
937 client.state.push_log(format!("Loadout: {err}"));
938 } else {
939 client
940 .state
941 .push_log(format!("T1 ← {}", preset.label));
942 }
943 }
944 }
945 InputAction::LoadoutAssignT2 => {
946 if !client.state.show_loadout_menu {
947 return;
948 }
949 let preset = {
950 let idx = client.state.loadout_menu_index;
951 client.state.rotation_presets.get(idx).cloned()
952 };
953 if let Some(preset) = preset {
954 let already = client
955 .state
956 .combat_slots
957 .iter()
958 .find(|s| s.slot_index == 2)
959 .and_then(|s| s.preset_id.as_deref())
960 == Some(preset.id.as_str());
961 if already {
962 client
963 .state
964 .push_log(format!("T2 already uses {}", preset.label));
965 } else if let Err(err) = client.assign_slot_preset(2, &preset.id).await {
966 client.state.push_log(format!("Loadout: {err}"));
967 } else {
968 client
969 .state
970 .push_log(format!("T2 ← {}", preset.label));
971 }
972 }
973 }
974 InputAction::CloseOverlay => {
975 client.state.show_loadout_menu = false;
976 client.state.show_rotation_editor = false;
977 client.state.rotation_editor.reset();
978 }
979 InputAction::ClearCombatTarget => {
980 if let Err(err) = client.clear_combat_target().await {
981 client.state.push_log(format!("Target: {err}"));
982 }
983 }
984 InputAction::ClearCombatTargetT2 => {
985 if let Err(err) = client.clear_combat_target_slot(2).await {
986 client.state.push_log(format!("T2 target: {err}"));
987 }
988 }
989 InputAction::Dodge => {
990 *ctx.auto_nav = None;
991 if let Err(err) = client.dodge().await {
992 client.state.push_log(format!("Dodge: {err}"));
993 }
994 }
995 InputAction::Lunge => {
996 *ctx.auto_nav = None;
997 let _ = client.stop().await;
998 if let Err(err) = client.lunge().await {
999 client.state.push_log(format!("Lunge: {err}"));
1000 }
1001 }
1002 InputAction::DirectionalJump { forward, strafe } => {
1003 *ctx.auto_nav = None;
1004 let _ = client.stop().await;
1005 if let Err(err) = client.directional_jump(forward, strafe).await {
1006 client.state.push_log(format!("Jump: {err}"));
1007 }
1008 }
1009 InputAction::CastHotbar { slot } => {
1010 if let Err(err) = client.cast_hotbar_ability(slot).await {
1011 client.state.push_log(format!("Hotbar {slot}: {err}"));
1012 }
1013 }
1014 InputAction::ToggleBlock => {
1015 *ctx.block_active = !*ctx.block_active;
1016 if let Err(err) = client.set_block(*ctx.block_active).await {
1017 client.state.push_log(format!("Block: {err}"));
1018 *ctx.block_active = false;
1019 }
1020 }
1021 InputAction::ToggleStats => client.toggle_stats(),
1022 InputAction::ToggleEquip => client.toggle_equip_menu(),
1023 InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
1024 InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
1025 InputAction::ToggleInventory => client.toggle_inventory_menu(),
1026 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
1027 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
1028 InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
1029 InputAction::QuestMenuUp => client.quest_menu_move(-1),
1030 InputAction::QuestMenuDown => client.quest_menu_move(1),
1031 InputAction::QuestWithdraw => client.quest_request_withdraw(),
1032 InputAction::RotationEditorBack => {
1033 let _ = client.back_on_esc();
1034 }
1035 InputAction::RotationEditorListUp
1036 | InputAction::RotationEditorListDown
1037 | InputAction::RotationEditorEdit
1038 | InputAction::RotationEditorNew
1039 | InputAction::RotationEditorDelete
1040 | InputAction::RotationEditorAddAbility
1041 | InputAction::RotationEditorRemoveAbility
1042 | InputAction::RotationEditorMoveAbilityUp
1043 | InputAction::RotationEditorMoveAbilityDown
1044 | InputAction::RotationEditorAbilityUp
1045 | InputAction::RotationEditorAbilityDown
1046 | InputAction::RotationEditorPickerUp
1047 | InputAction::RotationEditorPickerDown
1048 | InputAction::RotationEditorPickAbility
1049 | InputAction::RotationEditorRename
1050 | InputAction::RotationEditorConfirmLabel
1051 | InputAction::RotationEditorLabelBackspace
1052 | InputAction::RotationEditorLabelChar(_)
1053 | InputAction::RotationEditorSave => {
1054 handle_rotation_editor_action(client, action).await;
1055 }
1056 InputAction::None
1057 | InputAction::Quit
1058 | InputAction::ToggleHelp
1059 | InputAction::CycleHudView
1060 | InputAction::StartChat { .. }
1061 | InputAction::SubmitChat
1062 | InputAction::CancelChat
1063 | InputAction::ToggleSprintMode
1064 | InputAction::ToggleMapTarget
1065 | InputAction::ConfirmMapTarget
1066 | InputAction::CancelMapTarget
1067 | InputAction::MapTargetNudge { .. }
1068 | InputAction::CancelAutoNav
1069 | InputAction::StopMovement => {}
1070 }
1071}
1072
1073async fn handle_rotation_editor_action<S: PlayConnection>(
1074 client: &mut GameClient<S>,
1075 action: InputAction,
1076) {
1077 match action {
1078 InputAction::RotationEditorListUp => {
1079 if client.state.rotation_editor.list_index > 0 {
1080 client.state.rotation_editor.list_index -= 1;
1081 }
1082 }
1083 InputAction::RotationEditorListDown => {
1084 let max = client.state.rotation_presets.len();
1085 if max > 0 {
1086 client.state.rotation_editor.list_index =
1087 (client.state.rotation_editor.list_index + 1).min(max - 1);
1088 }
1089 }
1090 InputAction::RotationEditorEdit => {
1091 let idx = client.state.rotation_editor.list_index;
1092 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
1093 client.state.rotation_editor.draft = Some(preset);
1094 client.state.rotation_editor.ability_index = 0;
1095 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1096 }
1097 }
1098 InputAction::RotationEditorNew => {
1099 let preset = next_custom_preset(&client.state.rotation_presets);
1100 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
1101 client.state.rotation_editor.draft = Some(preset);
1102 client.state.rotation_editor.ability_index = 0;
1103 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1104 }
1105 InputAction::RotationEditorDelete => {
1106 let idx = client.state.rotation_editor.list_index;
1107 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
1108 if let Some(id) = preset_id {
1109 if preset_deletable(&id) {
1110 if let Err(err) = client.delete_rotation_preset(&id).await {
1111 client.state.push_log(format!("Delete: {err}"));
1112 } else {
1113 let max = client.state.rotation_presets.len();
1114 client.state.rotation_editor.list_index = if max == 0 {
1115 0
1116 } else {
1117 client.state.rotation_editor.list_index.min(max - 1)
1118 };
1119 }
1120 } else {
1121 client.state.push_log("Cannot delete built-in preset");
1122 }
1123 }
1124 }
1125 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
1126 RotationEditorMode::EditLabel => {
1127 client.state.rotation_editor.label_buffer.clear();
1128 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1129 }
1130 RotationEditorMode::PickAbility => {
1131 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1132 }
1133 RotationEditorMode::EditSequence => {
1134 client.state.rotation_editor.draft = None;
1135 client.state.rotation_editor.mode = RotationEditorMode::List;
1136 }
1137 RotationEditorMode::List => {}
1138 },
1139 InputAction::RotationEditorAddAbility => {
1140 client.state.rotation_editor.picker_index = 0;
1141 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
1142 }
1143 InputAction::RotationEditorRemoveAbility => {
1144 let idx = client.state.rotation_editor.ability_index;
1145 if let Some(draft) = &mut client.state.rotation_editor.draft {
1146 if idx < draft.abilities.len() {
1147 draft.abilities.remove(idx);
1148 if client.state.rotation_editor.ability_index >= draft.abilities.len()
1149 && client.state.rotation_editor.ability_index > 0
1150 {
1151 client.state.rotation_editor.ability_index -= 1;
1152 }
1153 }
1154 }
1155 }
1156 InputAction::RotationEditorMoveAbilityUp => {
1157 let i = client.state.rotation_editor.ability_index;
1158 if let Some(draft) = &mut client.state.rotation_editor.draft {
1159 if i > 0 && i < draft.abilities.len() {
1160 draft.abilities.swap(i, i - 1);
1161 client.state.rotation_editor.ability_index -= 1;
1162 }
1163 }
1164 }
1165 InputAction::RotationEditorMoveAbilityDown => {
1166 let i = client.state.rotation_editor.ability_index;
1167 if let Some(draft) = &mut client.state.rotation_editor.draft {
1168 if i + 1 < draft.abilities.len() {
1169 draft.abilities.swap(i, i + 1);
1170 client.state.rotation_editor.ability_index += 1;
1171 }
1172 }
1173 }
1174 InputAction::RotationEditorAbilityUp => {
1175 if client.state.rotation_editor.ability_index > 0 {
1176 client.state.rotation_editor.ability_index -= 1;
1177 }
1178 }
1179 InputAction::RotationEditorAbilityDown => {
1180 if let Some(draft) = &client.state.rotation_editor.draft {
1181 if !draft.abilities.is_empty() {
1182 client.state.rotation_editor.ability_index =
1183 (client.state.rotation_editor.ability_index + 1)
1184 .min(draft.abilities.len() - 1);
1185 }
1186 }
1187 }
1188 InputAction::RotationEditorPickerUp => {
1189 if client.state.rotation_editor.picker_index > 0 {
1190 client.state.rotation_editor.picker_index -= 1;
1191 }
1192 }
1193 InputAction::RotationEditorPickerDown => {
1194 let choices = editor_ability_choices(
1195 &client.state.known_abilities,
1196 &client.state.weapon_ability_id,
1197 );
1198 if !choices.is_empty() {
1199 client.state.rotation_editor.picker_index =
1200 (client.state.rotation_editor.picker_index + 1).min(choices.len() - 1);
1201 }
1202 }
1203 InputAction::RotationEditorPickAbility => {
1204 let choices = editor_ability_choices(
1205 &client.state.known_abilities,
1206 &client.state.weapon_ability_id,
1207 );
1208 let pick = client.state.rotation_editor.picker_index;
1209 if let Some(ability) = choices.get(pick) {
1210 if let Some(draft) = &mut client.state.rotation_editor.draft {
1211 let max = client.state.max_abilities_per_rotation.max(1) as usize;
1212 if draft.abilities.len() >= max {
1213 client
1214 .state
1215 .push_log(format!("Rotation full (max {max} from INT+WIS)"));
1216 } else {
1217 draft.abilities.push(ability.clone());
1218 client.state.rotation_editor.ability_index =
1219 draft.abilities.len().saturating_sub(1);
1220 }
1221 }
1222 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1223 }
1224 }
1225 InputAction::RotationEditorRename => {
1226 if let Some(draft) = &client.state.rotation_editor.draft {
1227 client.state.rotation_editor.label_buffer = draft.label.clone();
1228 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
1229 }
1230 }
1231 InputAction::RotationEditorConfirmLabel => {
1232 let label = client.state.rotation_editor.label_buffer.trim().to_string();
1233 if label.is_empty() {
1234 client.state.push_log("Label cannot be empty");
1235 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1236 draft.label = label;
1237 client.state.rotation_editor.label_buffer.clear();
1238 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1239 }
1240 }
1241 InputAction::RotationEditorLabelBackspace => {
1242 client.state.rotation_editor.label_buffer.pop();
1243 }
1244 InputAction::RotationEditorLabelChar(c) => {
1245 if client.state.rotation_editor.label_buffer.len() < 32 {
1246 client.state.rotation_editor.label_buffer.push(c);
1247 }
1248 }
1249 InputAction::RotationEditorSave => {
1250 let draft = match client.state.rotation_editor.draft.clone() {
1251 Some(d) => d,
1252 None => return,
1253 };
1254 if draft.abilities.is_empty() {
1255 client.state.push_log("Rotation needs at least one ability");
1256 return;
1257 }
1258 let saved_id = draft.id.clone();
1259 if let Err(err) = client.upsert_rotation_preset(draft).await {
1260 client.state.push_log(format!("Save: {err}"));
1261 } else {
1262 client.state.rotation_editor.mode = RotationEditorMode::List;
1263 client.state.rotation_editor.draft = None;
1264 if let Some(i) = client
1265 .state
1266 .rotation_presets
1267 .iter()
1268 .position(|p| p.id == saved_id)
1269 {
1270 client.state.rotation_editor.list_index = i;
1271 }
1272 }
1273 }
1274 _ => {}
1275 }
1276}