1use flatland_client_lib::{
4 AutoNavigator, ClientKeyBindings, GameClient, PlayConnection, RotationEditorMode,
5};
6use flatland_client_ui::{
7 next_custom_preset, preset_deletable, InputAction, UiKeyCode, UiKeyEvent, UiKeyEventKind,
8 EDITOR_ABILITIES,
9};
10
11pub struct ActionCtx<'a> {
12 pub block_active: &'a mut bool,
13 pub auto_nav: &'a mut Option<AutoNavigator>,
14}
15
16pub async fn dispatch_menu_key<S: PlayConnection>(
18 client: &mut GameClient<S>,
19 key: UiKeyEvent,
20) -> bool {
21 if key.kind != UiKeyEventKind::Press {
22 return false;
23 }
24
25 if client.state.show_worker_rename {
26 match key.code {
27 UiKeyCode::Esc => client.cancel_worker_rename(),
28 UiKeyCode::Enter => {
29 if let Err(err) = client.confirm_worker_rename().await {
30 client.state.push_log(format!("Rename: {err}"));
31 }
32 }
33 UiKeyCode::Backspace => {
34 client.state.rename_buffer.pop();
35 }
36 UiKeyCode::Char(c) => {
37 if client.state.rename_buffer.chars().count() < 32 {
38 client.state.rename_buffer.push(c);
39 }
40 }
41 _ => {}
42 }
43 return true;
44 }
45
46 if client.state.show_inventory_menu {
47 if client.state.show_rename_prompt {
48 match key.code {
49 UiKeyCode::Esc => client.cancel_rename_prompt(),
50 UiKeyCode::Enter => {
51 if let Err(err) = client.confirm_rename_prompt().await {
52 client.state.push_log(format!("Rename: {err}"));
53 }
54 }
55 UiKeyCode::Backspace => {
56 client.state.rename_buffer.pop();
57 }
58 UiKeyCode::Char(c) => {
59 if client.state.rename_buffer.len() < 32 {
60 client.state.rename_buffer.push(c);
61 }
62 }
63 _ => {}
64 }
65 } else if client.state.show_destroy_picker {
66 match key.code {
67 UiKeyCode::Esc => {
68 if client.state.destroy_confirm_pending {
69 client.cancel_destroy_confirm();
70 } else {
71 client.close_destroy_picker();
72 }
73 }
74 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
75 if !client.state.destroy_confirm_pending {
76 client.destroy_picker_adjust_quantity(-1);
77 }
78 }
79 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
80 if !client.state.destroy_confirm_pending {
81 client.destroy_picker_adjust_quantity(1);
82 }
83 }
84 UiKeyCode::Char('a') => {
85 if !client.state.destroy_confirm_pending {
86 client.destroy_picker_set_quantity_max();
87 }
88 }
89 UiKeyCode::Enter => {
90 if let Err(err) = client.activate_inventory_selection().await {
91 client.state.push_log(format!("Destroy failed: {err}"));
92 }
93 }
94 _ => {}
95 }
96 } else if client.state.show_move_picker {
97 match key.code {
98 UiKeyCode::Esc => client.close_move_picker(),
99 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
100 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
101 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
102 client.move_picker_adjust_quantity(-1);
103 }
104 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
105 client.move_picker_adjust_quantity(1);
106 }
107 UiKeyCode::Char('a') => client.move_picker_set_quantity_max(),
108 UiKeyCode::Enter => {
109 if let Err(err) = client.activate_inventory_selection().await {
110 client.state.push_log(format!("Move failed: {err}"));
111 }
112 }
113 _ => {}
114 }
115 } else {
116 match key.code {
117 UiKeyCode::Char('b') => client.close_inventory_menu(),
118 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
119 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
120 UiKeyCode::Enter => {
121 if let Err(err) = client.activate_inventory_selection().await {
122 client.state.push_log(format!("{err}"));
123 }
124 }
125 UiKeyCode::Char('m') => {
126 if let Err(err) = client.open_move_picker() {
127 client.state.push_log(format!("{err}"));
128 }
129 }
130 UiKeyCode::Char('e') => {
131 if let Err(err) = client.use_selected_consumable().await {
132 client.state.push_log(format!("Use: {err}"));
133 }
134 }
135 UiKeyCode::Char('n') => {
136 if let Err(err) = client.open_rename_prompt() {
137 client.state.push_log(format!("{err}"));
138 }
139 }
140 UiKeyCode::Char('d') => {
141 if let Err(err) = client.drop_selected().await {
142 client.state.push_log(format!("Drop: {err}"));
143 }
144 }
145 UiKeyCode::Char('x') => {
146 if let Err(err) = client.open_destroy_picker() {
147 client.state.push_log(format!("Destroy: {err}"));
148 }
149 }
150 UiKeyCode::Char('l') => {
151 if let Err(err) = client.toggle_chest_lock_for_selection().await {
152 client.state.push_log(format!("Lock: {err}"));
153 }
154 }
155 UiKeyCode::Char('g') => {
156 if let Err(err) = client.give_selected_inventory_to_worker().await {
157 client.state.push_log(format!("Give: {err}"));
158 }
159 }
160 UiKeyCode::Char('u') => {
161 if let Err(err) = client.unequip_mainhand().await {
162 client.state.push_log(format!("Unequip: {err}"));
163 }
164 }
165 _ => {}
166 }
167 }
168 return true;
169 }
170
171 if client.state.show_keychain_menu {
172 match key.code {
173 UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
174 UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
175 client.keychain_menu_move(-1);
176 }
177 UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
178 client.keychain_menu_move(1);
179 }
180 UiKeyCode::Enter => {
181 if let Err(err) = client.activate_keychain_selection().await {
182 client.state.push_log(format!("Keychain: {err}"));
183 }
184 }
185 _ => {}
186 }
187 return true;
188 }
189
190 if client.state.show_craft_menu {
191 match key.code {
192 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
193 client.close_craft_menu()
194 }
195 UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
196 UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
197 UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
198 UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
199 UiKeyCode::Char('a') => client.craft_batch_set_max(),
200 UiKeyCode::Enter => {
201 if let Err(err) = client.craft_menu_selection().await {
202 client.state.push_log(format!("Craft failed: {err}"));
203 }
204 }
205 _ => {}
206 }
207 return true;
208 }
209
210 if client.state.show_quest_offer {
211 match key.code {
212 UiKeyCode::Esc => client.quest_offer_decline(),
213 UiKeyCode::Enter => {
214 if let Err(err) = client.quest_offer_accept().await {
215 client.state.push_log(format!("Quest: {err}"));
216 }
217 }
218 _ => {}
219 }
220 return true;
221 }
222
223 if client.state.show_npc_chat {
224 match key.code {
225 UiKeyCode::Esc => {
226 if let Err(err) = client.npc_talk_close().await {
227 client.state.push_log(format!("Talk close failed: {err}"));
228 }
229 }
230 UiKeyCode::Enter => {
231 if let Err(err) = client.npc_talk_send().await {
232 client.state.push_log(format!("Talk failed: {err}"));
233 }
234 }
235 UiKeyCode::Backspace => {
236 if let Some(chat) = client.state.npc_chat.as_mut() {
237 chat.input.pop();
238 }
239 }
240 UiKeyCode::Char(c) => {
241 if let Some(chat) = client.state.npc_chat.as_mut() {
242 if !chat.pending && chat.input.len() < 300 {
243 chat.input.push(c);
244 }
245 }
246 }
247 _ => {}
248 }
249 return true;
250 }
251
252 if client.state.show_npc_verb_menu {
253 match key.code {
254 UiKeyCode::Esc => {
255 client.state.show_npc_verb_menu = false;
256 client.state.npc_verb_target = None;
257 }
258 UiKeyCode::Up | UiKeyCode::Char('k') => {
259 if client.state.npc_verb_index > 0 {
260 client.state.npc_verb_index -= 1;
261 }
262 }
263 UiKeyCode::Down | UiKeyCode::Char('j') => {
264 let max = client.npc_verb_options().len().saturating_sub(1);
265 if client.state.npc_verb_index < max {
266 client.state.npc_verb_index += 1;
267 }
268 }
269 UiKeyCode::Enter => {
270 if let Err(err) = client.confirm_npc_verb().await {
271 client.state.push_log(format!("Interact failed: {err}"));
272 }
273 }
274 _ => {}
275 }
276 return true;
277 }
278
279 if client.state.show_shop_menu {
280 match key.code {
281 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
282 client.back_from_shop_menu()
283 }
284 UiKeyCode::Tab => client.shop_tab_toggle(),
285 UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
286 UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
287 UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
288 UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
289 UiKeyCode::Char('a') => client.shop_quantity_set_max(),
290 UiKeyCode::Enter => {
291 if let Err(err) = client.shop_confirm().await {
292 client.state.push_log(format!("Trade failed: {err}"));
293 }
294 }
295 _ => {}
296 }
297 return true;
298 }
299
300 if client.state.show_quest_menu {
301 match key.code {
302 UiKeyCode::Esc => {
303 if client.state.quest_withdraw_confirm {
304 client.state.quest_withdraw_confirm = false;
305 } else {
306 client.state.show_quest_menu = false;
307 }
308 }
309 UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
310 UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
311 UiKeyCode::Char('x') => client.quest_request_withdraw(),
312 UiKeyCode::Enter => {
313 if let Err(err) = client.quest_confirm_action().await {
314 client.state.push_log(format!("Quest: {err}"));
315 }
316 }
317 _ => {}
318 }
319 return true;
320 }
321
322 if client.state.worker_route_editor.is_some() {
323 let at_root = client.re_at_root_sheet();
324 let sheet_index = client.re_sheet_index();
325 if at_root {
326 match key.code {
328 UiKeyCode::Esc => client.close_worker_route_editor(),
329 UiKeyCode::Char('s') => {
330 if let Err(err) = client.worker_route_editor_save().await {
331 client.state.push_log(format!("Route: {err}"));
332 }
333 }
334 UiKeyCode::Down => {
335 if key.modifiers.contains_control() {
336 client.worker_route_editor_move_selected(1);
337 } else {
338 client.worker_route_editor_select(1);
339 }
340 }
341 UiKeyCode::Up => {
342 if key.modifiers.contains_control() {
343 client.worker_route_editor_move_selected(-1);
344 } else {
345 client.worker_route_editor_select(-1);
346 }
347 }
348 UiKeyCode::Char('j') => {
351 if key.modifiers.contains_control() {
352 client.worker_route_editor_move_selected(-1);
353 } else {
354 client.worker_route_editor_select(1);
355 }
356 }
357 UiKeyCode::Char('k') => {
358 if key.modifiers.contains_control() {
359 client.worker_route_editor_move_selected(1);
360 } else {
361 client.worker_route_editor_select(-1);
362 }
363 }
364 UiKeyCode::Char('d') | UiKeyCode::Delete => {
365 client.worker_route_editor_delete_selected()
366 }
367 UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
368 UiKeyCode::Char('a') => client.re_open_add_menu(),
369 UiKeyCode::Char('l') => client.re_open_bed_picker(),
370 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
371 UiKeyCode::Enter => client.re_edit_selected_stop(),
372 _ => {}
373 }
374 } else {
375 match key.code {
377 UiKeyCode::Esc => client.re_sheet_back(),
378 UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
379 UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
380 UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
381 UiKeyCode::Enter | UiKeyCode::Char(' ') => {
382 client.re_sheet_row_activate(sheet_index)
383 }
384 UiKeyCode::Char('[') | UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
385 UiKeyCode::Char(']') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
386 _ => {}
387 }
388 }
389 return true;
390 }
391
392 if client.state.show_worker_give_picker {
393 match key.code {
394 UiKeyCode::Esc => client.close_worker_give_picker(),
395 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
396 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
397 UiKeyCode::Enter => {
398 if let Err(err) = client.confirm_worker_give_picker().await {
399 client.state.push_log(format!("Give: {err}"));
400 }
401 }
402 _ => {}
403 }
404 return true;
405 }
406
407 if client.state.show_worker_teach_picker {
408 match key.code {
409 UiKeyCode::Esc => client.close_worker_teach_picker(),
410 UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
411 UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
412 UiKeyCode::Enter => {
413 if let Err(err) = client.confirm_worker_teach_picker().await {
414 client.state.push_log(format!("Teach: {err}"));
415 }
416 }
417 _ => {}
418 }
419 return true;
420 }
421
422 if client.state.show_workers_menu {
423 match key.code {
424 UiKeyCode::Esc => client.state.show_workers_menu = false,
425 UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
426 UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
427 UiKeyCode::Char('d') => {
428 if let Err(err) = client.workers_dismiss_selected().await {
429 client.state.push_log(format!("Worker: {err}"));
430 }
431 }
432 UiKeyCode::Char('r') => {
433 if let Err(err) = client.hire_worker_laborer().await {
434 client.state.push_log(format!("Hire: {err}"));
435 }
436 }
437 UiKeyCode::Char('e') => {
438 if let Err(err) = client.open_worker_route_editor_for_selected() {
439 client.state.push_log(format!("Route: {err}"));
440 }
441 }
442 UiKeyCode::Char('n') => {
443 if let Err(err) = client.open_worker_rename() {
444 client.state.push_log(format!("Rename: {err}"));
445 }
446 }
447 UiKeyCode::Char('g') => {
448 if let Err(err) = client.open_worker_give_picker() {
449 client.state.push_log(format!("Give: {err}"));
450 }
451 }
452 UiKeyCode::Char('t') => {
453 if let Err(err) = client.open_worker_teach_picker() {
454 client.state.push_log(format!("Teach: {err}"));
455 }
456 }
457 UiKeyCode::Enter => {
458 if let Err(err) = client.workers_confirm_action().await {
459 client.state.push_log(format!("Worker: {err}"));
460 }
461 }
462 _ => {}
463 }
464 return true;
465 }
466
467 false
468}
469
470pub async fn dispatch_action<S: PlayConnection>(
471 client: &mut GameClient<S>,
472 keys: &ClientKeyBindings,
473 action: InputAction,
474 ctx: &mut ActionCtx<'_>,
475) {
476 match action {
477 InputAction::Harvest => {
478 if let Err(err) = client.harvest_nearest().await {
479 client.state.push_log(format!("Harvest failed: {err}"));
480 }
481 }
482 InputAction::Pickup => {
483 if client.pickup_nearest().await.is_err() {
484 if let Err(err) = client.pickup_nearest_container().await {
485 client.state.push_log(format!("Pickup: {err}"));
486 }
487 }
488 }
489 InputAction::Craft => client.open_craft_menu(),
490 InputAction::Interact | InputAction::UseWorld => {
491 if let Err(err) = client.use_nearest().await {
492 client.state.push_log(format!("Use: {err}"));
493 }
494 }
495 InputAction::TestDamage => {
496 if let Err(err) = client.test_damage(25.0).await {
497 client.state.push_log(format!("Damage failed: {err}"));
498 }
499 }
500 InputAction::CycleCombatTarget { reverse } => {
501 if let Err(err) = client.cycle_combat_target(reverse).await {
502 client.state.push_log(format!("T1 target: {err}"));
503 }
504 }
505 InputAction::CycleCombatTargetT2 { reverse } => {
506 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
507 client.state.push_log(format!("T2 target: {err}"));
508 }
509 }
510 InputAction::AdvanceRotationT1 => {
511 if let Err(err) = client.advance_rotation(1).await {
512 client.state.push_log(format!("T1 step: {err}"));
513 }
514 }
515 InputAction::AdvanceRotationT2 => {
516 if let Err(err) = client.advance_rotation(2).await {
517 client.state.push_log(format!("T2 step: {err}"));
518 }
519 }
520 InputAction::ToggleAutoT1 => {
521 if let Err(err) = client.toggle_auto_attack_slot(1).await {
522 client.state.push_log(format!("T1 auto: {err}"));
523 }
524 }
525 InputAction::ToggleAutoT2 => {
526 if let Err(err) = client.toggle_auto_attack_slot(2).await {
527 client.state.push_log(format!("T2 auto: {err}"));
528 }
529 }
530 InputAction::ToggleLoadout => {
531 client.state.show_rotation_editor = false;
532 client.state.rotation_editor.reset();
533 client.state.show_loadout_menu = !client.state.show_loadout_menu;
534 if client.state.show_loadout_menu {
535 *ctx.auto_nav = None;
536 let _ = client.stop().await;
537 }
538 }
539 InputAction::ToggleRotationEditor => {
540 client.state.show_loadout_menu = false;
541 if client.state.show_rotation_editor {
542 client.state.show_rotation_editor = false;
543 client.state.rotation_editor.reset();
544 } else {
545 client.state.show_rotation_editor = true;
546 client.state.rotation_editor.reset();
547 let max = client.state.rotation_presets.len();
548 if max > 0 {
549 client.state.rotation_editor.list_index =
550 client.state.rotation_editor.list_index.min(max - 1);
551 }
552 }
553 if client.state.show_rotation_editor {
554 *ctx.auto_nav = None;
555 let _ = client.stop().await;
556 }
557 }
558 InputAction::LoadoutMenuUp => {
559 if client.state.show_loadout_menu && client.state.loadout_menu_index > 0 {
560 client.state.loadout_menu_index -= 1;
561 }
562 }
563 InputAction::LoadoutMenuDown => {
564 if client.state.show_loadout_menu {
565 let max = client.state.rotation_presets.len();
566 if max > 0 {
567 client.state.loadout_menu_index =
568 (client.state.loadout_menu_index + 1).min(max - 1);
569 }
570 }
571 }
572 InputAction::LoadoutAssignT1 => {
573 let preset_id = {
574 let idx = client.state.loadout_menu_index;
575 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
576 };
577 if let Some(preset_id) = preset_id {
578 if let Err(err) = client.assign_slot_preset(1, &preset_id).await {
579 client.state.push_log(format!("Loadout: {err}"));
580 }
581 }
582 }
583 InputAction::LoadoutAssignT2 => {
584 let preset_id = {
585 let idx = client.state.loadout_menu_index;
586 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
587 };
588 if let Some(preset_id) = preset_id {
589 if let Err(err) = client.assign_slot_preset(2, &preset_id).await {
590 client.state.push_log(format!("Loadout: {err}"));
591 }
592 }
593 }
594 InputAction::CloseOverlay => {
595 client.state.show_loadout_menu = false;
596 client.state.show_rotation_editor = false;
597 client.state.rotation_editor.reset();
598 }
599 InputAction::ClearCombatTarget => {
600 if let Err(err) = client.clear_combat_target().await {
601 client.state.push_log(format!("Target: {err}"));
602 }
603 }
604 InputAction::ClearCombatTargetT2 => {
605 if let Err(err) = client.clear_combat_target_slot(2).await {
606 client.state.push_log(format!("T2 target: {err}"));
607 }
608 }
609 InputAction::Dodge => {
610 *ctx.auto_nav = None;
611 if let Err(err) = client.dodge().await {
612 client.state.push_log(format!("Dodge: {err}"));
613 }
614 }
615 InputAction::Lunge => {
616 *ctx.auto_nav = None;
617 let _ = client.stop().await;
618 if let Err(err) = client.lunge().await {
619 client.state.push_log(format!("Lunge: {err}"));
620 }
621 }
622 InputAction::DirectionalJump { forward, strafe } => {
623 *ctx.auto_nav = None;
624 let _ = client.stop().await;
625 if let Err(err) = client.directional_jump(forward, strafe).await {
626 client.state.push_log(format!("Jump: {err}"));
627 }
628 }
629 InputAction::CastHotbar { slot } => match keys.hotbar_ability(slot) {
630 Some(ability_id) => {
631 let ability_id = ability_id.to_string();
632 if let Err(err) = client.cast_hotbar_ability(&ability_id).await {
633 client.state.push_log(format!("Hotbar {slot}: {err}"));
634 }
635 }
636 None => {
637 client.state.push_log(format!(
638 "Hotbar {slot} unbound — set in client-settings.yaml"
639 ));
640 }
641 },
642 InputAction::ToggleBlock => {
643 *ctx.block_active = !*ctx.block_active;
644 if let Err(err) = client.set_block(*ctx.block_active).await {
645 client.state.push_log(format!("Block: {err}"));
646 *ctx.block_active = false;
647 }
648 }
649 InputAction::ToggleStats => client.toggle_stats(),
650 InputAction::ToggleInventory => client.toggle_inventory_menu(),
651 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
652 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
653 InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
654 InputAction::QuestMenuUp => client.quest_menu_move(-1),
655 InputAction::QuestMenuDown => client.quest_menu_move(1),
656 InputAction::QuestWithdraw => client.quest_request_withdraw(),
657 InputAction::RotationEditorBack => {
658 let _ = client.back_on_esc();
659 }
660 InputAction::RotationEditorListUp
661 | InputAction::RotationEditorListDown
662 | InputAction::RotationEditorEdit
663 | InputAction::RotationEditorNew
664 | InputAction::RotationEditorDelete
665 | InputAction::RotationEditorAddAbility
666 | InputAction::RotationEditorRemoveAbility
667 | InputAction::RotationEditorMoveAbilityUp
668 | InputAction::RotationEditorMoveAbilityDown
669 | InputAction::RotationEditorAbilityUp
670 | InputAction::RotationEditorAbilityDown
671 | InputAction::RotationEditorPickerUp
672 | InputAction::RotationEditorPickerDown
673 | InputAction::RotationEditorPickAbility
674 | InputAction::RotationEditorRename
675 | InputAction::RotationEditorConfirmLabel
676 | InputAction::RotationEditorLabelBackspace
677 | InputAction::RotationEditorLabelChar(_)
678 | InputAction::RotationEditorSave => {
679 handle_rotation_editor_action(client, action).await;
680 }
681 InputAction::None
682 | InputAction::Quit
683 | InputAction::ToggleHelp
684 | InputAction::CycleHudView
685 | InputAction::StartChat { .. }
686 | InputAction::SubmitChat
687 | InputAction::CancelChat
688 | InputAction::ToggleSprintMode
689 | InputAction::ToggleMapTarget
690 | InputAction::ConfirmMapTarget
691 | InputAction::CancelMapTarget
692 | InputAction::MapTargetNudge { .. }
693 | InputAction::CancelAutoNav
694 | InputAction::StopMovement => {}
695 }
696}
697
698async fn handle_rotation_editor_action<S: PlayConnection>(
699 client: &mut GameClient<S>,
700 action: InputAction,
701) {
702 match action {
703 InputAction::RotationEditorListUp => {
704 if client.state.rotation_editor.list_index > 0 {
705 client.state.rotation_editor.list_index -= 1;
706 }
707 }
708 InputAction::RotationEditorListDown => {
709 let max = client.state.rotation_presets.len();
710 if max > 0 {
711 client.state.rotation_editor.list_index =
712 (client.state.rotation_editor.list_index + 1).min(max - 1);
713 }
714 }
715 InputAction::RotationEditorEdit => {
716 let idx = client.state.rotation_editor.list_index;
717 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
718 client.state.rotation_editor.draft = Some(preset);
719 client.state.rotation_editor.ability_index = 0;
720 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
721 }
722 }
723 InputAction::RotationEditorNew => {
724 let preset = next_custom_preset(&client.state.rotation_presets);
725 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
726 client.state.rotation_editor.draft = Some(preset);
727 client.state.rotation_editor.ability_index = 0;
728 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
729 }
730 InputAction::RotationEditorDelete => {
731 let idx = client.state.rotation_editor.list_index;
732 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
733 if let Some(id) = preset_id {
734 if preset_deletable(&id) {
735 if let Err(err) = client.delete_rotation_preset(&id).await {
736 client.state.push_log(format!("Delete: {err}"));
737 } else {
738 let max = client.state.rotation_presets.len();
739 client.state.rotation_editor.list_index = if max == 0 {
740 0
741 } else {
742 client.state.rotation_editor.list_index.min(max - 1)
743 };
744 }
745 } else {
746 client.state.push_log("Cannot delete built-in preset");
747 }
748 }
749 }
750 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
751 RotationEditorMode::EditLabel => {
752 client.state.rotation_editor.label_buffer.clear();
753 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
754 }
755 RotationEditorMode::PickAbility => {
756 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
757 }
758 RotationEditorMode::EditSequence => {
759 client.state.rotation_editor.draft = None;
760 client.state.rotation_editor.mode = RotationEditorMode::List;
761 }
762 RotationEditorMode::List => {}
763 },
764 InputAction::RotationEditorAddAbility => {
765 client.state.rotation_editor.picker_index = 0;
766 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
767 }
768 InputAction::RotationEditorRemoveAbility => {
769 let idx = client.state.rotation_editor.ability_index;
770 if let Some(draft) = &mut client.state.rotation_editor.draft {
771 if idx < draft.abilities.len() {
772 draft.abilities.remove(idx);
773 if client.state.rotation_editor.ability_index >= draft.abilities.len()
774 && client.state.rotation_editor.ability_index > 0
775 {
776 client.state.rotation_editor.ability_index -= 1;
777 }
778 }
779 }
780 }
781 InputAction::RotationEditorMoveAbilityUp => {
782 let i = client.state.rotation_editor.ability_index;
783 if let Some(draft) = &mut client.state.rotation_editor.draft {
784 if i > 0 && i < draft.abilities.len() {
785 draft.abilities.swap(i, i - 1);
786 client.state.rotation_editor.ability_index -= 1;
787 }
788 }
789 }
790 InputAction::RotationEditorMoveAbilityDown => {
791 let i = client.state.rotation_editor.ability_index;
792 if let Some(draft) = &mut client.state.rotation_editor.draft {
793 if i + 1 < draft.abilities.len() {
794 draft.abilities.swap(i, i + 1);
795 client.state.rotation_editor.ability_index += 1;
796 }
797 }
798 }
799 InputAction::RotationEditorAbilityUp => {
800 if client.state.rotation_editor.ability_index > 0 {
801 client.state.rotation_editor.ability_index -= 1;
802 }
803 }
804 InputAction::RotationEditorAbilityDown => {
805 if let Some(draft) = &client.state.rotation_editor.draft {
806 if !draft.abilities.is_empty() {
807 client.state.rotation_editor.ability_index =
808 (client.state.rotation_editor.ability_index + 1)
809 .min(draft.abilities.len() - 1);
810 }
811 }
812 }
813 InputAction::RotationEditorPickerUp => {
814 if client.state.rotation_editor.picker_index > 0 {
815 client.state.rotation_editor.picker_index -= 1;
816 }
817 }
818 InputAction::RotationEditorPickerDown => {
819 if !EDITOR_ABILITIES.is_empty() {
820 client.state.rotation_editor.picker_index =
821 (client.state.rotation_editor.picker_index + 1).min(EDITOR_ABILITIES.len() - 1);
822 }
823 }
824 InputAction::RotationEditorPickAbility => {
825 let pick = client.state.rotation_editor.picker_index;
826 if let Some(ability) = EDITOR_ABILITIES.get(pick) {
827 if let Some(draft) = &mut client.state.rotation_editor.draft {
828 draft.abilities.push((*ability).to_string());
829 client.state.rotation_editor.ability_index =
830 draft.abilities.len().saturating_sub(1);
831 }
832 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
833 }
834 }
835 InputAction::RotationEditorRename => {
836 if let Some(draft) = &client.state.rotation_editor.draft {
837 client.state.rotation_editor.label_buffer = draft.label.clone();
838 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
839 }
840 }
841 InputAction::RotationEditorConfirmLabel => {
842 let label = client.state.rotation_editor.label_buffer.trim().to_string();
843 if label.is_empty() {
844 client.state.push_log("Label cannot be empty");
845 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
846 draft.label = label;
847 client.state.rotation_editor.label_buffer.clear();
848 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
849 }
850 }
851 InputAction::RotationEditorLabelBackspace => {
852 client.state.rotation_editor.label_buffer.pop();
853 }
854 InputAction::RotationEditorLabelChar(c) => {
855 if client.state.rotation_editor.label_buffer.len() < 32 {
856 client.state.rotation_editor.label_buffer.push(c);
857 }
858 }
859 InputAction::RotationEditorSave => {
860 let draft = match client.state.rotation_editor.draft.clone() {
861 Some(d) => d,
862 None => return,
863 };
864 if draft.abilities.is_empty() {
865 client.state.push_log("Rotation needs at least one ability");
866 return;
867 }
868 let saved_id = draft.id.clone();
869 if let Err(err) = client.upsert_rotation_preset(draft).await {
870 client.state.push_log(format!("Save: {err}"));
871 } else {
872 client.state.rotation_editor.mode = RotationEditorMode::List;
873 client.state.rotation_editor.draft = None;
874 if let Some(i) = client
875 .state
876 .rotation_presets
877 .iter()
878 .position(|p| p.id == saved_id)
879 {
880 client.state.rotation_editor.list_index = i;
881 }
882 }
883 }
884 _ => {}
885 }
886}