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::Char('[') | UiKeyCode::Char('-') => {
904 client.worker_take_picker_adjust_quantity(-1)
905 }
906 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
907 client.worker_take_picker_adjust_quantity(1)
908 }
909 UiKeyCode::Char('a') => client.worker_take_picker_set_quantity_max(),
910 UiKeyCode::Enter => {
911 if let Err(err) = client.confirm_worker_take_picker().await {
912 client.state.push_log(format!("Take: {err}"));
913 }
914 }
915 _ => {}
916 }
917 return true;
918 }
919
920 if client.state.show_worker_teach_picker {
921 match key.code {
922 UiKeyCode::Esc => client.close_worker_teach_picker(),
923 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
924 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
925 UiKeyCode::Enter => {
926 if let Err(err) = client.confirm_worker_teach_picker().await {
927 client.state.push_log(format!("Teach: {err}"));
928 }
929 }
930 _ => {}
931 }
932 return true;
933 }
934
935 if client.state.show_workers_menu {
936 match key.code {
937 UiKeyCode::Esc => client.state.show_workers_menu = false,
938 UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
939 UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
940 UiKeyCode::PageUp => client.workers_menu_page(-1),
941 UiKeyCode::PageDown => client.workers_menu_page(1),
942 UiKeyCode::Char('d') => {
943 if let Err(err) = client.workers_dismiss_selected().await {
944 client.state.push_log(format!("Worker: {err}"));
945 }
946 }
947 UiKeyCode::Char('r') => {
948 if let Err(err) = client.hire_worker_laborer().await {
949 client.state.push_log(format!("Hire: {err}"));
950 }
951 }
952 UiKeyCode::Char('e') => {
953 if let Err(err) = client.open_worker_route_editor_for_selected() {
954 client.state.push_log(format!("Route: {err}"));
955 }
956 }
957 UiKeyCode::Char('n') => {
958 if let Err(err) = client.open_worker_rename() {
959 client.state.push_log(format!("Rename: {err}"));
960 }
961 }
962 UiKeyCode::Char('g') => {
963 if let Err(err) = client.open_worker_give_picker() {
964 client.state.push_log(format!("Give: {err}"));
965 }
966 }
967 UiKeyCode::Char('i') => {
968 if let Err(err) = client.open_worker_take_picker() {
969 client.state.push_log(format!("Take: {err}"));
970 }
971 }
972 UiKeyCode::Char('t') => {
973 if let Err(err) = client.open_worker_teach_picker() {
974 client.state.push_log(format!("Teach: {err}"));
975 }
976 }
977 UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
978 UiKeyCode::Enter => {
979 if let Err(err) = client.workers_confirm_action().await {
980 client.state.push_log(format!("Worker: {err}"));
981 }
982 }
983 _ => {}
984 }
985 return true;
986 }
987
988 false
989}
990
991pub async fn dispatch_action<S: PlayConnection>(
992 client: &mut GameClient<S>,
993 _keys: &ClientKeyBindings,
994 action: InputAction,
995 ctx: &mut ActionCtx<'_>,
996) {
997 match action {
998 InputAction::StartChat { whisper } => {
999 if whisper {
1000 client.state.whisper_pouch_ui.open = false;
1002 client.state.social_chat.picking_stone = true;
1003 client.state.social_chat.stone_pick_index = 0;
1004 client.state.social_chat.input_focused = false;
1005 client.state.social_chat.push_system(
1006 "Whisper stones — ↑↓ select · Enter · Esc cancel",
1007 );
1008 } else {
1009 client.state.social_chat.focus_nearby();
1010 }
1011 }
1012 InputAction::Harvest => {
1013 if let Err(err) = client.harvest_nearest().await {
1014 client.state.push_log(format!("Harvest failed: {err}"));
1015 }
1016 }
1017 InputAction::Pickup => {
1018 if client.pickup_nearest().await.is_err() {
1019 if let Err(err) = client.pickup_nearest_container().await {
1020 client.state.push_log(format!("Pickup: {err}"));
1021 }
1022 }
1023 }
1024 InputAction::Craft => client.open_craft_menu(),
1025 InputAction::Interact | InputAction::UseWorld => {
1026 if let Err(err) = client.use_nearest().await {
1027 client.state.push_log(format!("Use: {err}"));
1028 }
1029 }
1030 InputAction::TestDamage => {
1031 if let Err(err) = client.test_damage(25.0).await {
1032 client.state.push_log(format!("Damage failed: {err}"));
1033 }
1034 }
1035 InputAction::CycleCombatTarget { reverse } => {
1036 if let Err(err) = client.cycle_combat_target(reverse).await {
1037 client.state.push_log(format!("T1 target: {err}"));
1038 }
1039 }
1040 InputAction::CycleCombatTargetT2 { reverse } => {
1041 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
1042 client.state.push_log(format!("T2 target: {err}"));
1043 }
1044 }
1045 InputAction::AdvanceRotationT1 => {
1046 if let Err(err) = client.advance_rotation(1).await {
1047 client.state.push_log(format!("T1 step: {err}"));
1048 }
1049 }
1050 InputAction::AdvanceRotationT2 => {
1051 if let Err(err) = client.advance_rotation(2).await {
1052 client.state.push_log(format!("T2 step: {err}"));
1053 }
1054 }
1055 InputAction::ToggleAutoT1 => {
1056 if let Err(err) = client.toggle_auto_attack_slot(1).await {
1057 client.state.push_log(format!("T1 auto: {err}"));
1058 }
1059 }
1060 InputAction::ToggleAutoT2 => {
1061 if let Err(err) = client.toggle_auto_attack_slot(2).await {
1062 client.state.push_log(format!("T2 auto: {err}"));
1063 }
1064 }
1065 InputAction::ToggleLoadout => {
1066 client.state.show_rotation_editor = false;
1067 client.state.rotation_editor.reset();
1068 client.state.show_loadout_menu = !client.state.show_loadout_menu;
1069 if client.state.show_loadout_menu {
1070 client.state.loadout_focus_presets = true;
1071 client.state.loadout_hotbar_slot = client.state.loadout_hotbar_slot.clamp(1, 9);
1072 let ability_len = client.state.loadout_hotbar_choices().len();
1073 if ability_len > 0 {
1074 client.state.loadout_ability_index =
1075 client.state.loadout_ability_index.min(ability_len - 1);
1076 } else {
1077 client.state.loadout_ability_index = 0;
1078 }
1079 let preset_len = client.state.rotation_presets.len();
1080 if preset_len > 0 {
1081 client.state.loadout_menu_index =
1082 client.state.loadout_menu_index.min(preset_len - 1);
1083 } else {
1084 client.state.loadout_menu_index = 0;
1085 }
1086 *ctx.auto_nav = None;
1087 let _ = client.stop().await;
1088 }
1089 }
1090 InputAction::ToggleRotationEditor => {
1091 client.state.show_loadout_menu = false;
1092 if client.state.show_rotation_editor {
1093 client.state.show_rotation_editor = false;
1094 client.state.rotation_editor.reset();
1095 } else {
1096 client.state.show_rotation_editor = true;
1097 client.state.rotation_editor.reset();
1098 let max = client.state.rotation_presets.len();
1099 if max > 0 {
1100 client.state.rotation_editor.list_index =
1101 client.state.rotation_editor.list_index.min(max - 1);
1102 }
1103 }
1104 if client.state.show_rotation_editor {
1105 *ctx.auto_nav = None;
1106 let _ = client.stop().await;
1107 }
1108 }
1109 InputAction::LoadoutMenuUp => {
1110 if !client.state.show_loadout_menu {
1111 return;
1112 }
1113 if client.state.loadout_focus_presets {
1114 if client.state.loadout_menu_index > 0 {
1115 client.state.loadout_menu_index -= 1;
1116 }
1117 } else if client.state.loadout_ability_index > 0 {
1118 client.state.loadout_ability_index -= 1;
1119 }
1120 }
1121 InputAction::LoadoutMenuDown => {
1122 if !client.state.show_loadout_menu {
1123 return;
1124 }
1125 if client.state.loadout_focus_presets {
1126 let max = client.state.rotation_presets.len();
1127 if max > 0 {
1128 client.state.loadout_menu_index =
1129 (client.state.loadout_menu_index + 1).min(max - 1);
1130 }
1131 } else {
1132 let max = client.state.loadout_hotbar_choices().len();
1133 if max > 0 {
1134 client.state.loadout_ability_index =
1135 (client.state.loadout_ability_index + 1).min(max - 1);
1136 }
1137 }
1138 }
1139 InputAction::LoadoutHotbarPrev => {
1140 if client.state.show_loadout_menu {
1141 let slot = client.state.loadout_hotbar_slot;
1142 client.state.loadout_hotbar_slot = if slot <= 1 { 9 } else { slot - 1 };
1143 }
1144 }
1145 InputAction::LoadoutHotbarNext => {
1146 if client.state.show_loadout_menu {
1147 let slot = client.state.loadout_hotbar_slot;
1148 client.state.loadout_hotbar_slot = if slot >= 9 { 1 } else { slot + 1 };
1149 }
1150 }
1151 InputAction::LoadoutToggleFocus => {
1152 if client.state.show_loadout_menu {
1153 client.state.loadout_focus_presets = !client.state.loadout_focus_presets;
1154 }
1155 }
1156 InputAction::LoadoutBindHotbar => {
1157 if !client.state.show_loadout_menu {
1158 return;
1159 }
1160 let slot = client.state.loadout_hotbar_slot;
1161 let binding = {
1162 let choices = client.state.loadout_hotbar_choices();
1163 choices
1164 .get(client.state.loadout_ability_index)
1165 .map(|c| c.binding.clone())
1166 };
1167 if let Some(binding) = binding {
1168 if let Err(err) = client.set_hotbar_slot(slot, Some(&binding)).await {
1169 client.state.push_log(format!("Hotbar: {err}"));
1170 }
1171 } else {
1172 client.state.push_log("No ability/consumable selected to bind");
1173 }
1174 }
1175 InputAction::LoadoutClearHotbar => {
1176 if !client.state.show_loadout_menu {
1177 return;
1178 }
1179 let slot = client.state.loadout_hotbar_slot;
1180 if let Err(err) = client.set_hotbar_slot(slot, None).await {
1181 client.state.push_log(format!("Hotbar: {err}"));
1182 }
1183 }
1184 InputAction::LoadoutAssignT1 => {
1185 if !client.state.show_loadout_menu {
1186 return;
1187 }
1188 let preset = {
1189 let idx = client.state.loadout_menu_index;
1190 client.state.rotation_presets.get(idx).cloned()
1191 };
1192 if let Some(preset) = preset {
1193 let already = client
1194 .state
1195 .combat_slots
1196 .iter()
1197 .find(|s| s.slot_index == 1)
1198 .and_then(|s| s.preset_id.as_deref())
1199 == Some(preset.id.as_str());
1200 if already {
1201 client.state.push_log(format!(
1202 "T1 already uses {} (equip weapons from inventory — Weapon auto tracks mainhand)",
1203 preset.label
1204 ));
1205 } else if let Err(err) = client.assign_slot_preset(1, &preset.id).await {
1206 client.state.push_log(format!("Loadout: {err}"));
1207 } else {
1208 client
1209 .state
1210 .push_log(format!("T1 ← {}", preset.label));
1211 }
1212 }
1213 }
1214 InputAction::LoadoutAssignT2 => {
1215 if !client.state.show_loadout_menu {
1216 return;
1217 }
1218 let preset = {
1219 let idx = client.state.loadout_menu_index;
1220 client.state.rotation_presets.get(idx).cloned()
1221 };
1222 if let Some(preset) = preset {
1223 let already = client
1224 .state
1225 .combat_slots
1226 .iter()
1227 .find(|s| s.slot_index == 2)
1228 .and_then(|s| s.preset_id.as_deref())
1229 == Some(preset.id.as_str());
1230 if already {
1231 client
1232 .state
1233 .push_log(format!("T2 already uses {}", preset.label));
1234 } else if let Err(err) = client.assign_slot_preset(2, &preset.id).await {
1235 client.state.push_log(format!("Loadout: {err}"));
1236 } else {
1237 client
1238 .state
1239 .push_log(format!("T2 ← {}", preset.label));
1240 }
1241 }
1242 }
1243 InputAction::CloseOverlay => {
1244 client.state.show_loadout_menu = false;
1245 client.state.show_rotation_editor = false;
1246 client.state.rotation_editor.reset();
1247 }
1248 InputAction::ClearCombatTarget => {
1249 if let Err(err) = client.clear_combat_target().await {
1250 client.state.push_log(format!("Target: {err}"));
1251 }
1252 }
1253 InputAction::ClearCombatTargetT2 => {
1254 if let Err(err) = client.clear_combat_target_slot(2).await {
1255 client.state.push_log(format!("T2 target: {err}"));
1256 }
1257 }
1258 InputAction::Dodge => {
1259 *ctx.auto_nav = None;
1260 if let Err(err) = client.dodge().await {
1261 client.state.push_log(format!("Dodge: {err}"));
1262 }
1263 }
1264 InputAction::Lunge => {
1265 *ctx.auto_nav = None;
1266 let _ = client.stop().await;
1267 if let Err(err) = client.lunge().await {
1268 client.state.push_log(format!("Lunge: {err}"));
1269 }
1270 }
1271 InputAction::DirectionalJump { forward, strafe } => {
1272 *ctx.auto_nav = None;
1273 let _ = client.stop().await;
1274 if let Err(err) = client.directional_jump(forward, strafe).await {
1275 client.state.push_log(format!("Jump: {err}"));
1276 }
1277 }
1278 InputAction::CastHotbar { slot } => {
1279 if let Err(err) = client.cast_hotbar_ability(slot).await {
1280 client.state.push_log(format!("Hotbar {slot}: {err}"));
1281 }
1282 }
1283 InputAction::ToggleBlock => {
1284 *ctx.block_active = !*ctx.block_active;
1285 if let Err(err) = client.set_block(*ctx.block_active).await {
1286 client.state.push_log(format!("Block: {err}"));
1287 *ctx.block_active = false;
1288 }
1289 }
1290 InputAction::ToggleStats => client.toggle_stats(),
1291 InputAction::ToggleEquip => client.toggle_equip_menu(),
1292 InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
1293 InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
1294 InputAction::ToggleInventory => client.toggle_inventory_menu(),
1295 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
1296 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
1297 InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
1298 InputAction::QuestMenuUp => client.quest_menu_move(-1),
1299 InputAction::QuestMenuDown => client.quest_menu_move(1),
1300 InputAction::QuestWithdraw => client.quest_request_withdraw(),
1301 InputAction::RotationEditorBack => {
1302 let _ = client.back_on_esc();
1303 }
1304 InputAction::RotationEditorListUp
1305 | InputAction::RotationEditorListDown
1306 | InputAction::RotationEditorEdit
1307 | InputAction::RotationEditorNew
1308 | InputAction::RotationEditorDelete
1309 | InputAction::RotationEditorAddAbility
1310 | InputAction::RotationEditorRemoveAbility
1311 | InputAction::RotationEditorMoveAbilityUp
1312 | InputAction::RotationEditorMoveAbilityDown
1313 | InputAction::RotationEditorAbilityUp
1314 | InputAction::RotationEditorAbilityDown
1315 | InputAction::RotationEditorPickerUp
1316 | InputAction::RotationEditorPickerDown
1317 | InputAction::RotationEditorPickAbility
1318 | InputAction::RotationEditorRename
1319 | InputAction::RotationEditorConfirmLabel
1320 | InputAction::RotationEditorLabelBackspace
1321 | InputAction::RotationEditorLabelChar(_)
1322 | InputAction::RotationEditorSave => {
1323 handle_rotation_editor_action(client, action).await;
1324 }
1325 InputAction::None
1326 | InputAction::Quit
1327 | InputAction::ToggleHelp
1328 | InputAction::CycleHudView
1329 | InputAction::SubmitChat
1330 | InputAction::CancelChat
1331 | InputAction::ToggleSprintMode
1332 | InputAction::ToggleMapTarget
1333 | InputAction::ConfirmMapTarget
1334 | InputAction::CancelMapTarget
1335 | InputAction::MapTargetNudge { .. }
1336 | InputAction::CancelAutoNav
1337 | InputAction::StopMovement => {}
1338 InputAction::ToggleHudLog => {
1339 client.state.hud_log_hidden = !client.state.hud_log_hidden;
1340 let mut cfg = flatland_client_lib::ClientConfig::load();
1341 let _ = cfg.save_hud_log_hidden(client.state.hud_log_hidden);
1342 if client.state.hud_log_hidden {
1343 client.state.push_log("LOG hidden — press ' to show");
1344 } else {
1345 client.state.push_log("LOG shown — press ' to hide");
1346 }
1347 }
1348 }
1349}
1350
1351async fn handle_rotation_editor_action<S: PlayConnection>(
1352 client: &mut GameClient<S>,
1353 action: InputAction,
1354) {
1355 match action {
1356 InputAction::RotationEditorListUp => {
1357 if client.state.rotation_editor.list_index > 0 {
1358 client.state.rotation_editor.list_index -= 1;
1359 }
1360 }
1361 InputAction::RotationEditorListDown => {
1362 let max = client.state.rotation_presets.len();
1363 if max > 0 {
1364 client.state.rotation_editor.list_index =
1365 (client.state.rotation_editor.list_index + 1).min(max - 1);
1366 }
1367 }
1368 InputAction::RotationEditorEdit => {
1369 let idx = client.state.rotation_editor.list_index;
1370 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
1371 client.state.rotation_editor.draft = Some(preset);
1372 client.state.rotation_editor.ability_index = 0;
1373 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1374 }
1375 }
1376 InputAction::RotationEditorNew => {
1377 let preset = next_custom_preset(&client.state.rotation_presets);
1378 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
1379 client.state.rotation_editor.draft = Some(preset);
1380 client.state.rotation_editor.ability_index = 0;
1381 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1382 }
1383 InputAction::RotationEditorDelete => {
1384 let idx = client.state.rotation_editor.list_index;
1385 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
1386 if let Some(id) = preset_id {
1387 if preset_deletable(&id) {
1388 if let Err(err) = client.delete_rotation_preset(&id).await {
1389 client.state.push_log(format!("Delete: {err}"));
1390 } else {
1391 let max = client.state.rotation_presets.len();
1392 client.state.rotation_editor.list_index = if max == 0 {
1393 0
1394 } else {
1395 client.state.rotation_editor.list_index.min(max - 1)
1396 };
1397 }
1398 } else {
1399 client.state.push_log("Cannot delete built-in preset");
1400 }
1401 }
1402 }
1403 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
1404 RotationEditorMode::EditLabel => {
1405 client.state.rotation_editor.label_buffer.clear();
1406 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1407 }
1408 RotationEditorMode::PickAbility => {
1409 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1410 }
1411 RotationEditorMode::EditSequence => {
1412 client.state.rotation_editor.draft = None;
1413 client.state.rotation_editor.mode = RotationEditorMode::List;
1414 }
1415 RotationEditorMode::List => {}
1416 },
1417 InputAction::RotationEditorAddAbility => {
1418 client.state.rotation_editor.picker_index = 0;
1419 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
1420 }
1421 InputAction::RotationEditorRemoveAbility => {
1422 let idx = client.state.rotation_editor.ability_index;
1423 if let Some(draft) = &mut client.state.rotation_editor.draft {
1424 if idx < draft.abilities.len() {
1425 draft.abilities.remove(idx);
1426 if client.state.rotation_editor.ability_index >= draft.abilities.len()
1427 && client.state.rotation_editor.ability_index > 0
1428 {
1429 client.state.rotation_editor.ability_index -= 1;
1430 }
1431 }
1432 }
1433 }
1434 InputAction::RotationEditorMoveAbilityUp => {
1435 let i = client.state.rotation_editor.ability_index;
1436 if let Some(draft) = &mut client.state.rotation_editor.draft {
1437 if i > 0 && i < draft.abilities.len() {
1438 draft.abilities.swap(i, i - 1);
1439 client.state.rotation_editor.ability_index -= 1;
1440 }
1441 }
1442 }
1443 InputAction::RotationEditorMoveAbilityDown => {
1444 let i = client.state.rotation_editor.ability_index;
1445 if let Some(draft) = &mut client.state.rotation_editor.draft {
1446 if i + 1 < draft.abilities.len() {
1447 draft.abilities.swap(i, i + 1);
1448 client.state.rotation_editor.ability_index += 1;
1449 }
1450 }
1451 }
1452 InputAction::RotationEditorAbilityUp => {
1453 if client.state.rotation_editor.ability_index > 0 {
1454 client.state.rotation_editor.ability_index -= 1;
1455 }
1456 }
1457 InputAction::RotationEditorAbilityDown => {
1458 if let Some(draft) = &client.state.rotation_editor.draft {
1459 if !draft.abilities.is_empty() {
1460 client.state.rotation_editor.ability_index =
1461 (client.state.rotation_editor.ability_index + 1)
1462 .min(draft.abilities.len() - 1);
1463 }
1464 }
1465 }
1466 InputAction::RotationEditorPickerUp => {
1467 if client.state.rotation_editor.picker_index > 0 {
1468 client.state.rotation_editor.picker_index -= 1;
1469 }
1470 }
1471 InputAction::RotationEditorPickerDown => {
1472 let choices = editor_ability_choices(
1473 &client.state.known_abilities,
1474 &client.state.weapon_ability_id,
1475 );
1476 if !choices.is_empty() {
1477 client.state.rotation_editor.picker_index =
1478 (client.state.rotation_editor.picker_index + 1).min(choices.len() - 1);
1479 }
1480 }
1481 InputAction::RotationEditorPickAbility => {
1482 let choices = editor_ability_choices(
1483 &client.state.known_abilities,
1484 &client.state.weapon_ability_id,
1485 );
1486 let pick = client.state.rotation_editor.picker_index;
1487 if let Some(ability) = choices.get(pick) {
1488 if let Some(draft) = &mut client.state.rotation_editor.draft {
1489 let max = client.state.max_abilities_per_rotation.max(1) as usize;
1490 if draft.abilities.len() >= max {
1491 client
1492 .state
1493 .push_log(format!("Rotation full (max {max} from INT+WIS)"));
1494 } else {
1495 draft.abilities.push(ability.clone());
1496 client.state.rotation_editor.ability_index =
1497 draft.abilities.len().saturating_sub(1);
1498 }
1499 }
1500 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1501 }
1502 }
1503 InputAction::RotationEditorRename => {
1504 if let Some(draft) = &client.state.rotation_editor.draft {
1505 client.state.rotation_editor.label_buffer = draft.label.clone();
1506 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
1507 }
1508 }
1509 InputAction::RotationEditorConfirmLabel => {
1510 let label = client.state.rotation_editor.label_buffer.trim().to_string();
1511 if label.is_empty() {
1512 client.state.push_log("Label cannot be empty");
1513 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1514 draft.label = label;
1515 client.state.rotation_editor.label_buffer.clear();
1516 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1517 }
1518 }
1519 InputAction::RotationEditorLabelBackspace => {
1520 client.state.rotation_editor.label_buffer.pop();
1521 }
1522 InputAction::RotationEditorLabelChar(c) => {
1523 if client.state.rotation_editor.label_buffer.len() < 32 {
1524 client.state.rotation_editor.label_buffer.push(c);
1525 }
1526 }
1527 InputAction::RotationEditorSave => {
1528 let draft = match client.state.rotation_editor.draft.clone() {
1529 Some(d) => d,
1530 None => return,
1531 };
1532 if draft.abilities.is_empty() {
1533 client.state.push_log("Rotation needs at least one ability");
1534 return;
1535 }
1536 let saved_id = draft.id.clone();
1537 if let Err(err) = client.upsert_rotation_preset(draft).await {
1538 client.state.push_log(format!("Save: {err}"));
1539 } else {
1540 client.state.rotation_editor.mode = RotationEditorMode::List;
1541 client.state.rotation_editor.draft = None;
1542 if let Some(i) = client
1543 .state
1544 .rotation_presets
1545 .iter()
1546 .position(|p| p.id == saved_id)
1547 {
1548 client.state.rotation_editor.list_index = i;
1549 }
1550 }
1551 }
1552 _ => {}
1553 }
1554}