use flatland_client_lib::{AutoNavigator, GameClient, RemoteSession, RotationEditorMode};
use flatland_client_ui::{
next_custom_preset, preset_deletable, InputAction, UiKeyCode, UiKeyEvent, UiKeyEventKind,
EDITOR_ABILITIES,
};
pub struct ActionCtx<'a> {
pub block_active: &'a mut bool,
pub auto_nav: &'a mut Option<AutoNavigator>,
}
pub async fn dispatch_menu_key(
client: &mut GameClient<RemoteSession>,
key: UiKeyEvent,
) -> bool {
if key.kind != UiKeyEventKind::Press {
return false;
}
if client.state.show_inventory_menu {
if client.state.show_rename_prompt {
match key.code {
UiKeyCode::Esc => client.cancel_rename_prompt(),
UiKeyCode::Enter => {
if let Err(err) = client.confirm_rename_prompt().await {
client.state.push_log(format!("Rename: {err}"));
}
}
UiKeyCode::Backspace => {
client.state.rename_buffer.pop();
}
UiKeyCode::Char(c) => {
if client.state.rename_buffer.len() < 32 {
client.state.rename_buffer.push(c);
}
}
_ => {}
}
} else if client.state.show_destroy_picker {
match key.code {
UiKeyCode::Esc => {
if client.state.destroy_confirm_pending {
client.cancel_destroy_confirm();
} else {
client.close_destroy_picker();
}
}
UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
if !client.state.destroy_confirm_pending {
client.destroy_picker_adjust_quantity(-1);
}
}
UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
if !client.state.destroy_confirm_pending {
client.destroy_picker_adjust_quantity(1);
}
}
UiKeyCode::Char('a') => {
if !client.state.destroy_confirm_pending {
client.destroy_picker_set_quantity_max();
}
}
UiKeyCode::Enter => {
if let Err(err) = client.activate_inventory_selection().await {
client.state.push_log(format!("Destroy failed: {err}"));
}
}
_ => {}
}
} else if client.state.show_move_picker {
match key.code {
UiKeyCode::Esc => client.close_move_picker(),
UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
client.move_picker_adjust_quantity(-1);
}
UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
client.move_picker_adjust_quantity(1);
}
UiKeyCode::Char('a') => client.move_picker_set_quantity_max(),
UiKeyCode::Enter => {
if let Err(err) = client.activate_inventory_selection().await {
client.state.push_log(format!("Move failed: {err}"));
}
}
_ => {}
}
} else {
match key.code {
UiKeyCode::Char('b') => client.close_inventory_menu(),
UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
UiKeyCode::Enter => {
if let Err(err) = client.activate_inventory_selection().await {
client.state.push_log(format!("{err}"));
}
}
UiKeyCode::Char('m') => {
if let Err(err) = client.open_move_picker() {
client.state.push_log(format!("{err}"));
}
}
UiKeyCode::Char('n') => {
if let Err(err) = client.open_rename_prompt() {
client.state.push_log(format!("{err}"));
}
}
UiKeyCode::Char('d') => {
if let Err(err) = client.drop_selected().await {
client.state.push_log(format!("Drop: {err}"));
}
}
UiKeyCode::Char('x') => {
if let Err(err) = client.open_destroy_picker() {
client.state.push_log(format!("Destroy: {err}"));
}
}
UiKeyCode::Char('l') => {
if let Err(err) = client.toggle_chest_lock_for_selection().await {
client.state.push_log(format!("Lock: {err}"));
}
}
UiKeyCode::Char('u') => {
if let Err(err) = client.unequip_mainhand().await {
client.state.push_log(format!("Unequip: {err}"));
}
}
_ => {}
}
}
return true;
}
if client.state.show_keychain_menu {
match key.code {
UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
client.keychain_menu_move(-1);
}
UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
client.keychain_menu_move(1);
}
UiKeyCode::Enter => {
if let Err(err) = client.activate_keychain_selection().await {
client.state.push_log(format!("Keychain: {err}"));
}
}
_ => {}
}
return true;
}
if client.state.show_craft_menu {
match key.code {
UiKeyCode::Esc
| UiKeyCode::Char('[')
| UiKeyCode::Char('n')
| UiKeyCode::Char('c') => client.close_craft_menu(),
UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
UiKeyCode::Char('a') => client.craft_batch_set_max(),
UiKeyCode::Enter => {
if let Err(err) = client.craft_menu_selection().await {
client.state.push_log(format!("Craft failed: {err}"));
}
}
_ => {}
}
return true;
}
if client.state.show_npc_chat {
match key.code {
UiKeyCode::Esc => {
if let Err(err) = client.npc_talk_close().await {
client.state.push_log(format!("Talk close failed: {err}"));
}
}
UiKeyCode::Enter => {
if let Err(err) = client.npc_talk_send().await {
client.state.push_log(format!("Talk failed: {err}"));
}
}
UiKeyCode::Backspace => {
if let Some(chat) = client.state.npc_chat.as_mut() {
chat.input.pop();
}
}
UiKeyCode::Char(c) => {
if let Some(chat) = client.state.npc_chat.as_mut() {
if !chat.pending && chat.input.len() < 300 {
chat.input.push(c);
}
}
}
_ => {}
}
return true;
}
if client.state.show_npc_verb_menu {
match key.code {
UiKeyCode::Esc => {
client.state.show_npc_verb_menu = false;
client.state.npc_verb_target = None;
}
UiKeyCode::Up | UiKeyCode::Char('k') => {
if client.state.npc_verb_index > 0 {
client.state.npc_verb_index -= 1;
}
}
UiKeyCode::Down | UiKeyCode::Char('j') => {
let max = client.npc_verb_options().len().saturating_sub(1);
if client.state.npc_verb_index < max {
client.state.npc_verb_index += 1;
}
}
UiKeyCode::Enter => {
if let Err(err) = client.confirm_npc_verb().await {
client.state.push_log(format!("Interact failed: {err}"));
}
}
_ => {}
}
return true;
}
if client.state.show_shop_menu {
match key.code {
UiKeyCode::Esc
| UiKeyCode::Char('[')
| UiKeyCode::Char('n')
| UiKeyCode::Char('c') => client.close_shop_menu(),
UiKeyCode::Tab => client.shop_tab_toggle(),
UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
UiKeyCode::Char('a') => client.shop_quantity_set_max(),
UiKeyCode::Enter => {
if let Err(err) = client.shop_confirm().await {
client.state.push_log(format!("Trade failed: {err}"));
}
}
_ => {}
}
return true;
}
if client.state.show_quest_offer {
match key.code {
UiKeyCode::Esc => client.quest_offer_decline(),
UiKeyCode::Enter => {
if let Err(err) = client.quest_offer_accept().await {
client.state.push_log(format!("Quest: {err}"));
}
}
_ => {}
}
return true;
}
if client.state.show_quest_menu {
match key.code {
UiKeyCode::Esc => {
if client.state.quest_withdraw_confirm {
client.state.quest_withdraw_confirm = false;
} else {
client.state.show_quest_menu = false;
}
}
UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
UiKeyCode::Char('x') => client.quest_request_withdraw(),
UiKeyCode::Enter => {
if let Err(err) = client.quest_confirm_action().await {
client.state.push_log(format!("Quest: {err}"));
}
}
_ => {}
}
return true;
}
false
}
pub async fn dispatch_action(
client: &mut GameClient<RemoteSession>,
keys: &flatland_client_lib::ClientKeyBindings,
action: InputAction,
ctx: &mut ActionCtx<'_>,
) {
match action {
InputAction::Harvest => {
if let Err(err) = client.harvest_nearest().await {
client.state.push_log(format!("Harvest failed: {err}"));
}
}
InputAction::Pickup => {
if let Err(_) = client.pickup_nearest().await {
if let Err(err) = client.pickup_nearest_container().await {
client.state.push_log(format!("Pickup: {err}"));
}
}
}
InputAction::Craft => client.open_craft_menu(),
InputAction::Interact | InputAction::UseWorld => {
if let Err(err) = client.use_nearest().await {
client.state.push_log(format!("Use: {err}"));
}
}
InputAction::TestDamage => {
if let Err(err) = client.test_damage(25.0).await {
client.state.push_log(format!("Damage failed: {err}"));
}
}
InputAction::CycleCombatTarget { reverse } => {
if let Err(err) = client.cycle_combat_target(reverse).await {
client.state.push_log(format!("T1 target: {err}"));
}
}
InputAction::CycleCombatTargetT2 { reverse } => {
if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
client.state.push_log(format!("T2 target: {err}"));
}
}
InputAction::AdvanceRotationT1 => {
if let Err(err) = client.advance_rotation(1).await {
client.state.push_log(format!("T1 step: {err}"));
}
}
InputAction::AdvanceRotationT2 => {
if let Err(err) = client.advance_rotation(2).await {
client.state.push_log(format!("T2 step: {err}"));
}
}
InputAction::ToggleAutoT1 => {
if let Err(err) = client.toggle_auto_attack_slot(1).await {
client.state.push_log(format!("T1 auto: {err}"));
}
}
InputAction::ToggleAutoT2 => {
if let Err(err) = client.toggle_auto_attack_slot(2).await {
client.state.push_log(format!("T2 auto: {err}"));
}
}
InputAction::ToggleLoadout => {
client.state.show_rotation_editor = false;
client.state.rotation_editor.reset();
client.state.show_loadout_menu = !client.state.show_loadout_menu;
if client.state.show_loadout_menu {
*ctx.auto_nav = None;
let _ = client.stop().await;
}
}
InputAction::ToggleRotationEditor => {
client.state.show_loadout_menu = false;
if client.state.show_rotation_editor {
client.state.show_rotation_editor = false;
client.state.rotation_editor.reset();
} else {
client.state.show_rotation_editor = true;
client.state.rotation_editor.reset();
let max = client.state.rotation_presets.len();
if max > 0 {
client.state.rotation_editor.list_index =
client.state.rotation_editor.list_index.min(max - 1);
}
}
if client.state.show_rotation_editor {
*ctx.auto_nav = None;
let _ = client.stop().await;
}
}
InputAction::LoadoutMenuUp => {
if client.state.show_loadout_menu && client.state.loadout_menu_index > 0 {
client.state.loadout_menu_index -= 1;
}
}
InputAction::LoadoutMenuDown => {
if client.state.show_loadout_menu {
let max = client.state.rotation_presets.len();
if max > 0 {
client.state.loadout_menu_index =
(client.state.loadout_menu_index + 1).min(max - 1);
}
}
}
InputAction::LoadoutAssignT1 => {
let preset_id = {
let idx = client.state.loadout_menu_index;
client
.state
.rotation_presets
.get(idx)
.map(|p| p.id.clone())
};
if let Some(preset_id) = preset_id {
if let Err(err) = client.assign_slot_preset(1, &preset_id).await {
client.state.push_log(format!("Loadout: {err}"));
}
}
}
InputAction::LoadoutAssignT2 => {
let preset_id = {
let idx = client.state.loadout_menu_index;
client
.state
.rotation_presets
.get(idx)
.map(|p| p.id.clone())
};
if let Some(preset_id) = preset_id {
if let Err(err) = client.assign_slot_preset(2, &preset_id).await {
client.state.push_log(format!("Loadout: {err}"));
}
}
}
InputAction::CloseOverlay => {
client.state.show_loadout_menu = false;
client.state.show_rotation_editor = false;
client.state.rotation_editor.reset();
}
InputAction::ClearCombatTarget => {
if let Err(err) = client.clear_combat_target().await {
client.state.push_log(format!("Target: {err}"));
}
}
InputAction::ClearCombatTargetT2 => {
if let Err(err) = client.clear_combat_target_slot(2).await {
client.state.push_log(format!("T2 target: {err}"));
}
}
InputAction::Dodge => {
*ctx.auto_nav = None;
if let Err(err) = client.dodge().await {
client.state.push_log(format!("Dodge: {err}"));
}
}
InputAction::Lunge => {
*ctx.auto_nav = None;
let _ = client.stop().await;
if let Err(err) = client.lunge().await {
client.state.push_log(format!("Lunge: {err}"));
}
}
InputAction::CastHotbar { slot } => {
match keys.hotbar_ability(slot) {
Some(ability_id) => {
let ability_id = ability_id.to_string();
if let Err(err) = client.cast_hotbar_ability(&ability_id).await {
client.state.push_log(format!("Hotbar {slot}: {err}"));
}
}
None => {
client.state.push_log(format!(
"Hotbar {slot} unbound — set in client-settings.yaml"
));
}
}
}
InputAction::ToggleBlock => {
*ctx.block_active = !*ctx.block_active;
if let Err(err) = client.set_block(*ctx.block_active).await {
client.state.push_log(format!("Block: {err}"));
*ctx.block_active = false;
}
}
InputAction::ToggleStats => client.toggle_stats(),
InputAction::ToggleInventory => client.toggle_inventory_menu(),
InputAction::ToggleKeychain => client.toggle_keychain_menu(),
InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
InputAction::QuestMenuUp => client.quest_menu_move(-1),
InputAction::QuestMenuDown => client.quest_menu_move(1),
InputAction::QuestWithdraw => client.quest_request_withdraw(),
InputAction::RotationEditorBack => {
let _ = client.back_on_esc();
}
InputAction::RotationEditorListUp
| InputAction::RotationEditorListDown
| InputAction::RotationEditorEdit
| InputAction::RotationEditorNew
| InputAction::RotationEditorDelete
| InputAction::RotationEditorAddAbility
| InputAction::RotationEditorRemoveAbility
| InputAction::RotationEditorMoveAbilityUp
| InputAction::RotationEditorMoveAbilityDown
| InputAction::RotationEditorAbilityUp
| InputAction::RotationEditorAbilityDown
| InputAction::RotationEditorPickerUp
| InputAction::RotationEditorPickerDown
| InputAction::RotationEditorPickAbility
| InputAction::RotationEditorRename
| InputAction::RotationEditorConfirmLabel
| InputAction::RotationEditorLabelBackspace
| InputAction::RotationEditorLabelChar(_)
| InputAction::RotationEditorSave => {
handle_rotation_editor_action(client, action).await;
}
InputAction::None
| InputAction::Quit
| InputAction::ToggleHelp
| InputAction::CycleHudView
| InputAction::StartChat { .. }
| InputAction::SubmitChat
| InputAction::CancelChat
| InputAction::ToggleSprintMode
| InputAction::ToggleMapTarget
| InputAction::ConfirmMapTarget
| InputAction::CancelMapTarget
| InputAction::MapTargetNudge { .. }
| InputAction::CancelAutoNav
| InputAction::StopMovement => {}
}
}
async fn handle_rotation_editor_action(
client: &mut GameClient<RemoteSession>,
action: InputAction,
) {
match action {
InputAction::RotationEditorListUp => {
if client.state.rotation_editor.list_index > 0 {
client.state.rotation_editor.list_index -= 1;
}
}
InputAction::RotationEditorListDown => {
let max = client.state.rotation_presets.len();
if max > 0 {
client.state.rotation_editor.list_index =
(client.state.rotation_editor.list_index + 1).min(max - 1);
}
}
InputAction::RotationEditorEdit => {
let idx = client.state.rotation_editor.list_index;
if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
client.state.rotation_editor.draft = Some(preset);
client.state.rotation_editor.ability_index = 0;
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
}
InputAction::RotationEditorNew => {
let preset = next_custom_preset(&client.state.rotation_presets);
client.state.rotation_editor.list_index = client.state.rotation_presets.len();
client.state.rotation_editor.draft = Some(preset);
client.state.rotation_editor.ability_index = 0;
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
InputAction::RotationEditorDelete => {
let idx = client.state.rotation_editor.list_index;
let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
if let Some(id) = preset_id {
if preset_deletable(&id) {
if let Err(err) = client.delete_rotation_preset(&id).await {
client.state.push_log(format!("Delete: {err}"));
} else {
let max = client.state.rotation_presets.len();
client.state.rotation_editor.list_index = if max == 0 {
0
} else {
client.state.rotation_editor.list_index.min(max - 1)
};
}
} else {
client.state.push_log("Cannot delete built-in preset");
}
}
}
InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
RotationEditorMode::EditLabel => {
client.state.rotation_editor.label_buffer.clear();
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
RotationEditorMode::PickAbility => {
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
RotationEditorMode::EditSequence => {
client.state.rotation_editor.draft = None;
client.state.rotation_editor.mode = RotationEditorMode::List;
}
RotationEditorMode::List => {}
},
InputAction::RotationEditorAddAbility => {
client.state.rotation_editor.picker_index = 0;
client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
}
InputAction::RotationEditorRemoveAbility => {
let idx = client.state.rotation_editor.ability_index;
if let Some(draft) = &mut client.state.rotation_editor.draft {
if idx < draft.abilities.len() {
draft.abilities.remove(idx);
if client.state.rotation_editor.ability_index >= draft.abilities.len()
&& client.state.rotation_editor.ability_index > 0
{
client.state.rotation_editor.ability_index -= 1;
}
}
}
}
InputAction::RotationEditorMoveAbilityUp => {
let i = client.state.rotation_editor.ability_index;
if let Some(draft) = &mut client.state.rotation_editor.draft {
if i > 0 && i < draft.abilities.len() {
draft.abilities.swap(i, i - 1);
client.state.rotation_editor.ability_index -= 1;
}
}
}
InputAction::RotationEditorMoveAbilityDown => {
let i = client.state.rotation_editor.ability_index;
if let Some(draft) = &mut client.state.rotation_editor.draft {
if i + 1 < draft.abilities.len() {
draft.abilities.swap(i, i + 1);
client.state.rotation_editor.ability_index += 1;
}
}
}
InputAction::RotationEditorAbilityUp => {
if client.state.rotation_editor.ability_index > 0 {
client.state.rotation_editor.ability_index -= 1;
}
}
InputAction::RotationEditorAbilityDown => {
if let Some(draft) = &client.state.rotation_editor.draft {
if !draft.abilities.is_empty() {
client.state.rotation_editor.ability_index = (client
.state
.rotation_editor
.ability_index
+ 1)
.min(draft.abilities.len() - 1);
}
}
}
InputAction::RotationEditorPickerUp => {
if client.state.rotation_editor.picker_index > 0 {
client.state.rotation_editor.picker_index -= 1;
}
}
InputAction::RotationEditorPickerDown => {
if !EDITOR_ABILITIES.is_empty() {
client.state.rotation_editor.picker_index = (client
.state
.rotation_editor
.picker_index
+ 1)
.min(EDITOR_ABILITIES.len() - 1);
}
}
InputAction::RotationEditorPickAbility => {
let pick = client.state.rotation_editor.picker_index;
if let Some(ability) = EDITOR_ABILITIES.get(pick) {
if let Some(draft) = &mut client.state.rotation_editor.draft {
draft.abilities.push((*ability).to_string());
client.state.rotation_editor.ability_index =
draft.abilities.len().saturating_sub(1);
}
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
}
InputAction::RotationEditorRename => {
if let Some(draft) = &client.state.rotation_editor.draft {
client.state.rotation_editor.label_buffer = draft.label.clone();
client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
}
}
InputAction::RotationEditorConfirmLabel => {
let label = client.state.rotation_editor.label_buffer.trim().to_string();
if label.is_empty() {
client.state.push_log("Label cannot be empty");
} else if let Some(draft) = &mut client.state.rotation_editor.draft {
draft.label = label;
client.state.rotation_editor.label_buffer.clear();
client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
}
}
InputAction::RotationEditorLabelBackspace => {
client.state.rotation_editor.label_buffer.pop();
}
InputAction::RotationEditorLabelChar(c) => {
if client.state.rotation_editor.label_buffer.len() < 32 {
client.state.rotation_editor.label_buffer.push(c);
}
}
InputAction::RotationEditorSave => {
let draft = match client.state.rotation_editor.draft.clone() {
Some(d) => d,
None => return,
};
if draft.abilities.is_empty() {
client.state.push_log("Rotation needs at least one ability");
return;
}
let saved_id = draft.id.clone();
if let Err(err) = client.upsert_rotation_preset(draft).await {
client.state.push_log(format!("Save: {err}"));
} else {
client.state.rotation_editor.mode = RotationEditorMode::List;
client.state.rotation_editor.draft = None;
if let Some(i) = client
.state
.rotation_presets
.iter()
.position(|p| p.id == saved_id)
{
client.state.rotation_editor.list_index = i;
}
}
}
_ => {}
}
}