flatland_play_loop/
actions.rs1use 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_inventory_menu {
26 if client.state.show_rename_prompt {
27 match key.code {
28 UiKeyCode::Esc => client.cancel_rename_prompt(),
29 UiKeyCode::Enter => {
30 if let Err(err) = client.confirm_rename_prompt().await {
31 client.state.push_log(format!("Rename: {err}"));
32 }
33 }
34 UiKeyCode::Backspace => {
35 client.state.rename_buffer.pop();
36 }
37 UiKeyCode::Char(c) => {
38 if client.state.rename_buffer.len() < 32 {
39 client.state.rename_buffer.push(c);
40 }
41 }
42 _ => {}
43 }
44 } else if client.state.show_destroy_picker {
45 match key.code {
46 UiKeyCode::Esc => {
47 if client.state.destroy_confirm_pending {
48 client.cancel_destroy_confirm();
49 } else {
50 client.close_destroy_picker();
51 }
52 }
53 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
54 if !client.state.destroy_confirm_pending {
55 client.destroy_picker_adjust_quantity(-1);
56 }
57 }
58 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
59 if !client.state.destroy_confirm_pending {
60 client.destroy_picker_adjust_quantity(1);
61 }
62 }
63 UiKeyCode::Char('a') => {
64 if !client.state.destroy_confirm_pending {
65 client.destroy_picker_set_quantity_max();
66 }
67 }
68 UiKeyCode::Enter => {
69 if let Err(err) = client.activate_inventory_selection().await {
70 client.state.push_log(format!("Destroy failed: {err}"));
71 }
72 }
73 _ => {}
74 }
75 } else if client.state.show_move_picker {
76 match key.code {
77 UiKeyCode::Esc => client.close_move_picker(),
78 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
79 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
80 UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
81 client.move_picker_adjust_quantity(-1);
82 }
83 UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
84 client.move_picker_adjust_quantity(1);
85 }
86 UiKeyCode::Char('a') => client.move_picker_set_quantity_max(),
87 UiKeyCode::Enter => {
88 if let Err(err) = client.activate_inventory_selection().await {
89 client.state.push_log(format!("Move failed: {err}"));
90 }
91 }
92 _ => {}
93 }
94 } else {
95 match key.code {
96 UiKeyCode::Char('b') => client.close_inventory_menu(),
97 UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
98 UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
99 UiKeyCode::Enter => {
100 if let Err(err) = client.activate_inventory_selection().await {
101 client.state.push_log(format!("{err}"));
102 }
103 }
104 UiKeyCode::Char('m') => {
105 if let Err(err) = client.open_move_picker() {
106 client.state.push_log(format!("{err}"));
107 }
108 }
109 UiKeyCode::Char('n') => {
110 if let Err(err) = client.open_rename_prompt() {
111 client.state.push_log(format!("{err}"));
112 }
113 }
114 UiKeyCode::Char('d') => {
115 if let Err(err) = client.drop_selected().await {
116 client.state.push_log(format!("Drop: {err}"));
117 }
118 }
119 UiKeyCode::Char('x') => {
120 if let Err(err) = client.open_destroy_picker() {
121 client.state.push_log(format!("Destroy: {err}"));
122 }
123 }
124 UiKeyCode::Char('l') => {
125 if let Err(err) = client.toggle_chest_lock_for_selection().await {
126 client.state.push_log(format!("Lock: {err}"));
127 }
128 }
129 UiKeyCode::Char('u') => {
130 if let Err(err) = client.unequip_mainhand().await {
131 client.state.push_log(format!("Unequip: {err}"));
132 }
133 }
134 _ => {}
135 }
136 }
137 return true;
138 }
139
140 if client.state.show_keychain_menu {
141 match key.code {
142 UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
143 UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
144 client.keychain_menu_move(-1);
145 }
146 UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
147 client.keychain_menu_move(1);
148 }
149 UiKeyCode::Enter => {
150 if let Err(err) = client.activate_keychain_selection().await {
151 client.state.push_log(format!("Keychain: {err}"));
152 }
153 }
154 _ => {}
155 }
156 return true;
157 }
158
159 if client.state.show_craft_menu {
160 match key.code {
161 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
162 client.close_craft_menu()
163 }
164 UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
165 UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
166 UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
167 UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
168 UiKeyCode::Char('a') => client.craft_batch_set_max(),
169 UiKeyCode::Enter => {
170 if let Err(err) = client.craft_menu_selection().await {
171 client.state.push_log(format!("Craft failed: {err}"));
172 }
173 }
174 _ => {}
175 }
176 return true;
177 }
178
179 if client.state.show_quest_offer {
180 match key.code {
181 UiKeyCode::Esc => client.quest_offer_decline(),
182 UiKeyCode::Enter => {
183 if let Err(err) = client.quest_offer_accept().await {
184 client.state.push_log(format!("Quest: {err}"));
185 }
186 }
187 _ => {}
188 }
189 return true;
190 }
191
192 if client.state.show_npc_chat {
193 match key.code {
194 UiKeyCode::Esc => {
195 if let Err(err) = client.npc_talk_close().await {
196 client.state.push_log(format!("Talk close failed: {err}"));
197 }
198 }
199 UiKeyCode::Enter => {
200 if let Err(err) = client.npc_talk_send().await {
201 client.state.push_log(format!("Talk failed: {err}"));
202 }
203 }
204 UiKeyCode::Backspace => {
205 if let Some(chat) = client.state.npc_chat.as_mut() {
206 chat.input.pop();
207 }
208 }
209 UiKeyCode::Char(c) => {
210 if let Some(chat) = client.state.npc_chat.as_mut() {
211 if !chat.pending && chat.input.len() < 300 {
212 chat.input.push(c);
213 }
214 }
215 }
216 _ => {}
217 }
218 return true;
219 }
220
221 if client.state.show_npc_verb_menu {
222 match key.code {
223 UiKeyCode::Esc => {
224 client.state.show_npc_verb_menu = false;
225 client.state.npc_verb_target = None;
226 }
227 UiKeyCode::Up | UiKeyCode::Char('k') => {
228 if client.state.npc_verb_index > 0 {
229 client.state.npc_verb_index -= 1;
230 }
231 }
232 UiKeyCode::Down | UiKeyCode::Char('j') => {
233 let max = client.npc_verb_options().len().saturating_sub(1);
234 if client.state.npc_verb_index < max {
235 client.state.npc_verb_index += 1;
236 }
237 }
238 UiKeyCode::Enter => {
239 if let Err(err) = client.confirm_npc_verb().await {
240 client.state.push_log(format!("Interact failed: {err}"));
241 }
242 }
243 _ => {}
244 }
245 return true;
246 }
247
248 if client.state.show_shop_menu {
249 match key.code {
250 UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
251 client.back_from_shop_menu()
252 }
253 UiKeyCode::Tab => client.shop_tab_toggle(),
254 UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
255 UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
256 UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
257 UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
258 UiKeyCode::Char('a') => client.shop_quantity_set_max(),
259 UiKeyCode::Enter => {
260 if let Err(err) = client.shop_confirm().await {
261 client.state.push_log(format!("Trade failed: {err}"));
262 }
263 }
264 _ => {}
265 }
266 return true;
267 }
268
269 if client.state.show_quest_menu {
270 match key.code {
271 UiKeyCode::Esc => {
272 if client.state.quest_withdraw_confirm {
273 client.state.quest_withdraw_confirm = false;
274 } else {
275 client.state.show_quest_menu = false;
276 }
277 }
278 UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
279 UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
280 UiKeyCode::Char('x') => client.quest_request_withdraw(),
281 UiKeyCode::Enter => {
282 if let Err(err) = client.quest_confirm_action().await {
283 client.state.push_log(format!("Quest: {err}"));
284 }
285 }
286 _ => {}
287 }
288 return true;
289 }
290
291 false
292}
293
294pub async fn dispatch_action<S: PlayConnection>(
295 client: &mut GameClient<S>,
296 keys: &ClientKeyBindings,
297 action: InputAction,
298 ctx: &mut ActionCtx<'_>,
299) {
300 match action {
301 InputAction::Harvest => {
302 if let Err(err) = client.harvest_nearest().await {
303 client.state.push_log(format!("Harvest failed: {err}"));
304 }
305 }
306 InputAction::Pickup => {
307 if client.pickup_nearest().await.is_err() {
308 if let Err(err) = client.pickup_nearest_container().await {
309 client.state.push_log(format!("Pickup: {err}"));
310 }
311 }
312 }
313 InputAction::Craft => client.open_craft_menu(),
314 InputAction::Interact | InputAction::UseWorld => {
315 if let Err(err) = client.use_nearest().await {
316 client.state.push_log(format!("Use: {err}"));
317 }
318 }
319 InputAction::TestDamage => {
320 if let Err(err) = client.test_damage(25.0).await {
321 client.state.push_log(format!("Damage failed: {err}"));
322 }
323 }
324 InputAction::CycleCombatTarget { reverse } => {
325 if let Err(err) = client.cycle_combat_target(reverse).await {
326 client.state.push_log(format!("T1 target: {err}"));
327 }
328 }
329 InputAction::CycleCombatTargetT2 { reverse } => {
330 if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
331 client.state.push_log(format!("T2 target: {err}"));
332 }
333 }
334 InputAction::AdvanceRotationT1 => {
335 if let Err(err) = client.advance_rotation(1).await {
336 client.state.push_log(format!("T1 step: {err}"));
337 }
338 }
339 InputAction::AdvanceRotationT2 => {
340 if let Err(err) = client.advance_rotation(2).await {
341 client.state.push_log(format!("T2 step: {err}"));
342 }
343 }
344 InputAction::ToggleAutoT1 => {
345 if let Err(err) = client.toggle_auto_attack_slot(1).await {
346 client.state.push_log(format!("T1 auto: {err}"));
347 }
348 }
349 InputAction::ToggleAutoT2 => {
350 if let Err(err) = client.toggle_auto_attack_slot(2).await {
351 client.state.push_log(format!("T2 auto: {err}"));
352 }
353 }
354 InputAction::ToggleLoadout => {
355 client.state.show_rotation_editor = false;
356 client.state.rotation_editor.reset();
357 client.state.show_loadout_menu = !client.state.show_loadout_menu;
358 if client.state.show_loadout_menu {
359 *ctx.auto_nav = None;
360 let _ = client.stop().await;
361 }
362 }
363 InputAction::ToggleRotationEditor => {
364 client.state.show_loadout_menu = false;
365 if client.state.show_rotation_editor {
366 client.state.show_rotation_editor = false;
367 client.state.rotation_editor.reset();
368 } else {
369 client.state.show_rotation_editor = true;
370 client.state.rotation_editor.reset();
371 let max = client.state.rotation_presets.len();
372 if max > 0 {
373 client.state.rotation_editor.list_index =
374 client.state.rotation_editor.list_index.min(max - 1);
375 }
376 }
377 if client.state.show_rotation_editor {
378 *ctx.auto_nav = None;
379 let _ = client.stop().await;
380 }
381 }
382 InputAction::LoadoutMenuUp => {
383 if client.state.show_loadout_menu && client.state.loadout_menu_index > 0 {
384 client.state.loadout_menu_index -= 1;
385 }
386 }
387 InputAction::LoadoutMenuDown => {
388 if client.state.show_loadout_menu {
389 let max = client.state.rotation_presets.len();
390 if max > 0 {
391 client.state.loadout_menu_index =
392 (client.state.loadout_menu_index + 1).min(max - 1);
393 }
394 }
395 }
396 InputAction::LoadoutAssignT1 => {
397 let preset_id = {
398 let idx = client.state.loadout_menu_index;
399 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
400 };
401 if let Some(preset_id) = preset_id {
402 if let Err(err) = client.assign_slot_preset(1, &preset_id).await {
403 client.state.push_log(format!("Loadout: {err}"));
404 }
405 }
406 }
407 InputAction::LoadoutAssignT2 => {
408 let preset_id = {
409 let idx = client.state.loadout_menu_index;
410 client.state.rotation_presets.get(idx).map(|p| p.id.clone())
411 };
412 if let Some(preset_id) = preset_id {
413 if let Err(err) = client.assign_slot_preset(2, &preset_id).await {
414 client.state.push_log(format!("Loadout: {err}"));
415 }
416 }
417 }
418 InputAction::CloseOverlay => {
419 client.state.show_loadout_menu = false;
420 client.state.show_rotation_editor = false;
421 client.state.rotation_editor.reset();
422 }
423 InputAction::ClearCombatTarget => {
424 if let Err(err) = client.clear_combat_target().await {
425 client.state.push_log(format!("Target: {err}"));
426 }
427 }
428 InputAction::ClearCombatTargetT2 => {
429 if let Err(err) = client.clear_combat_target_slot(2).await {
430 client.state.push_log(format!("T2 target: {err}"));
431 }
432 }
433 InputAction::Dodge => {
434 *ctx.auto_nav = None;
435 if let Err(err) = client.dodge().await {
436 client.state.push_log(format!("Dodge: {err}"));
437 }
438 }
439 InputAction::Lunge => {
440 *ctx.auto_nav = None;
441 let _ = client.stop().await;
442 if let Err(err) = client.lunge().await {
443 client.state.push_log(format!("Lunge: {err}"));
444 }
445 }
446 InputAction::DirectionalJump { forward, strafe } => {
447 *ctx.auto_nav = None;
448 let _ = client.stop().await;
449 if let Err(err) = client.directional_jump(forward, strafe).await {
450 client.state.push_log(format!("Jump: {err}"));
451 }
452 }
453 InputAction::CastHotbar { slot } => match keys.hotbar_ability(slot) {
454 Some(ability_id) => {
455 let ability_id = ability_id.to_string();
456 if let Err(err) = client.cast_hotbar_ability(&ability_id).await {
457 client.state.push_log(format!("Hotbar {slot}: {err}"));
458 }
459 }
460 None => {
461 client.state.push_log(format!(
462 "Hotbar {slot} unbound — set in client-settings.yaml"
463 ));
464 }
465 },
466 InputAction::ToggleBlock => {
467 *ctx.block_active = !*ctx.block_active;
468 if let Err(err) = client.set_block(*ctx.block_active).await {
469 client.state.push_log(format!("Block: {err}"));
470 *ctx.block_active = false;
471 }
472 }
473 InputAction::ToggleStats => client.toggle_stats(),
474 InputAction::ToggleInventory => client.toggle_inventory_menu(),
475 InputAction::ToggleKeychain => client.toggle_keychain_menu(),
476 InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
477 InputAction::QuestMenuUp => client.quest_menu_move(-1),
478 InputAction::QuestMenuDown => client.quest_menu_move(1),
479 InputAction::QuestWithdraw => client.quest_request_withdraw(),
480 InputAction::RotationEditorBack => {
481 let _ = client.back_on_esc();
482 }
483 InputAction::RotationEditorListUp
484 | InputAction::RotationEditorListDown
485 | InputAction::RotationEditorEdit
486 | InputAction::RotationEditorNew
487 | InputAction::RotationEditorDelete
488 | InputAction::RotationEditorAddAbility
489 | InputAction::RotationEditorRemoveAbility
490 | InputAction::RotationEditorMoveAbilityUp
491 | InputAction::RotationEditorMoveAbilityDown
492 | InputAction::RotationEditorAbilityUp
493 | InputAction::RotationEditorAbilityDown
494 | InputAction::RotationEditorPickerUp
495 | InputAction::RotationEditorPickerDown
496 | InputAction::RotationEditorPickAbility
497 | InputAction::RotationEditorRename
498 | InputAction::RotationEditorConfirmLabel
499 | InputAction::RotationEditorLabelBackspace
500 | InputAction::RotationEditorLabelChar(_)
501 | InputAction::RotationEditorSave => {
502 handle_rotation_editor_action(client, action).await;
503 }
504 InputAction::None
505 | InputAction::Quit
506 | InputAction::ToggleHelp
507 | InputAction::CycleHudView
508 | InputAction::StartChat { .. }
509 | InputAction::SubmitChat
510 | InputAction::CancelChat
511 | InputAction::ToggleSprintMode
512 | InputAction::ToggleMapTarget
513 | InputAction::ConfirmMapTarget
514 | InputAction::CancelMapTarget
515 | InputAction::MapTargetNudge { .. }
516 | InputAction::CancelAutoNav
517 | InputAction::StopMovement => {}
518 }
519}
520
521async fn handle_rotation_editor_action<S: PlayConnection>(
522 client: &mut GameClient<S>,
523 action: InputAction,
524) {
525 match action {
526 InputAction::RotationEditorListUp => {
527 if client.state.rotation_editor.list_index > 0 {
528 client.state.rotation_editor.list_index -= 1;
529 }
530 }
531 InputAction::RotationEditorListDown => {
532 let max = client.state.rotation_presets.len();
533 if max > 0 {
534 client.state.rotation_editor.list_index =
535 (client.state.rotation_editor.list_index + 1).min(max - 1);
536 }
537 }
538 InputAction::RotationEditorEdit => {
539 let idx = client.state.rotation_editor.list_index;
540 if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
541 client.state.rotation_editor.draft = Some(preset);
542 client.state.rotation_editor.ability_index = 0;
543 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
544 }
545 }
546 InputAction::RotationEditorNew => {
547 let preset = next_custom_preset(&client.state.rotation_presets);
548 client.state.rotation_editor.list_index = client.state.rotation_presets.len();
549 client.state.rotation_editor.draft = Some(preset);
550 client.state.rotation_editor.ability_index = 0;
551 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
552 }
553 InputAction::RotationEditorDelete => {
554 let idx = client.state.rotation_editor.list_index;
555 let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
556 if let Some(id) = preset_id {
557 if preset_deletable(&id) {
558 if let Err(err) = client.delete_rotation_preset(&id).await {
559 client.state.push_log(format!("Delete: {err}"));
560 } else {
561 let max = client.state.rotation_presets.len();
562 client.state.rotation_editor.list_index = if max == 0 {
563 0
564 } else {
565 client.state.rotation_editor.list_index.min(max - 1)
566 };
567 }
568 } else {
569 client.state.push_log("Cannot delete built-in preset");
570 }
571 }
572 }
573 InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
574 RotationEditorMode::EditLabel => {
575 client.state.rotation_editor.label_buffer.clear();
576 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
577 }
578 RotationEditorMode::PickAbility => {
579 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
580 }
581 RotationEditorMode::EditSequence => {
582 client.state.rotation_editor.draft = None;
583 client.state.rotation_editor.mode = RotationEditorMode::List;
584 }
585 RotationEditorMode::List => {}
586 },
587 InputAction::RotationEditorAddAbility => {
588 client.state.rotation_editor.picker_index = 0;
589 client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
590 }
591 InputAction::RotationEditorRemoveAbility => {
592 let idx = client.state.rotation_editor.ability_index;
593 if let Some(draft) = &mut client.state.rotation_editor.draft {
594 if idx < draft.abilities.len() {
595 draft.abilities.remove(idx);
596 if client.state.rotation_editor.ability_index >= draft.abilities.len()
597 && client.state.rotation_editor.ability_index > 0
598 {
599 client.state.rotation_editor.ability_index -= 1;
600 }
601 }
602 }
603 }
604 InputAction::RotationEditorMoveAbilityUp => {
605 let i = client.state.rotation_editor.ability_index;
606 if let Some(draft) = &mut client.state.rotation_editor.draft {
607 if i > 0 && i < draft.abilities.len() {
608 draft.abilities.swap(i, i - 1);
609 client.state.rotation_editor.ability_index -= 1;
610 }
611 }
612 }
613 InputAction::RotationEditorMoveAbilityDown => {
614 let i = client.state.rotation_editor.ability_index;
615 if let Some(draft) = &mut client.state.rotation_editor.draft {
616 if i + 1 < draft.abilities.len() {
617 draft.abilities.swap(i, i + 1);
618 client.state.rotation_editor.ability_index += 1;
619 }
620 }
621 }
622 InputAction::RotationEditorAbilityUp => {
623 if client.state.rotation_editor.ability_index > 0 {
624 client.state.rotation_editor.ability_index -= 1;
625 }
626 }
627 InputAction::RotationEditorAbilityDown => {
628 if let Some(draft) = &client.state.rotation_editor.draft {
629 if !draft.abilities.is_empty() {
630 client.state.rotation_editor.ability_index =
631 (client.state.rotation_editor.ability_index + 1)
632 .min(draft.abilities.len() - 1);
633 }
634 }
635 }
636 InputAction::RotationEditorPickerUp => {
637 if client.state.rotation_editor.picker_index > 0 {
638 client.state.rotation_editor.picker_index -= 1;
639 }
640 }
641 InputAction::RotationEditorPickerDown => {
642 if !EDITOR_ABILITIES.is_empty() {
643 client.state.rotation_editor.picker_index =
644 (client.state.rotation_editor.picker_index + 1).min(EDITOR_ABILITIES.len() - 1);
645 }
646 }
647 InputAction::RotationEditorPickAbility => {
648 let pick = client.state.rotation_editor.picker_index;
649 if let Some(ability) = EDITOR_ABILITIES.get(pick) {
650 if let Some(draft) = &mut client.state.rotation_editor.draft {
651 draft.abilities.push((*ability).to_string());
652 client.state.rotation_editor.ability_index =
653 draft.abilities.len().saturating_sub(1);
654 }
655 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
656 }
657 }
658 InputAction::RotationEditorRename => {
659 if let Some(draft) = &client.state.rotation_editor.draft {
660 client.state.rotation_editor.label_buffer = draft.label.clone();
661 client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
662 }
663 }
664 InputAction::RotationEditorConfirmLabel => {
665 let label = client.state.rotation_editor.label_buffer.trim().to_string();
666 if label.is_empty() {
667 client.state.push_log("Label cannot be empty");
668 } else if let Some(draft) = &mut client.state.rotation_editor.draft {
669 draft.label = label;
670 client.state.rotation_editor.label_buffer.clear();
671 client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
672 }
673 }
674 InputAction::RotationEditorLabelBackspace => {
675 client.state.rotation_editor.label_buffer.pop();
676 }
677 InputAction::RotationEditorLabelChar(c) => {
678 if client.state.rotation_editor.label_buffer.len() < 32 {
679 client.state.rotation_editor.label_buffer.push(c);
680 }
681 }
682 InputAction::RotationEditorSave => {
683 let draft = match client.state.rotation_editor.draft.clone() {
684 Some(d) => d,
685 None => return,
686 };
687 if draft.abilities.is_empty() {
688 client.state.push_log("Rotation needs at least one ability");
689 return;
690 }
691 let saved_id = draft.id.clone();
692 if let Err(err) = client.upsert_rotation_preset(draft).await {
693 client.state.push_log(format!("Save: {err}"));
694 } else {
695 client.state.rotation_editor.mode = RotationEditorMode::List;
696 client.state.rotation_editor.draft = None;
697 if let Some(i) = client
698 .state
699 .rotation_presets
700 .iter()
701 .position(|p| p.id == saved_id)
702 {
703 client.state.rotation_editor.list_index = i;
704 }
705 }
706 }
707 _ => {}
708 }
709}