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