diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 2d882528bd..e2ea60b4ec 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -85,7 +85,7 @@ - + @@ -238,7 +238,7 @@ - + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index 7482bf6f5e..f95d5fe6ad 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -570,7 +570,7 @@ 源文件\Vision\Battle - + 源文件\Vision\Battle @@ -971,7 +971,7 @@ 源文件\Vision\Battle - + 源文件\Vision\Battle diff --git a/src/MaaCore/Task/BattleHelper.cpp b/src/MaaCore/Task/BattleHelper.cpp index 95b39d40ec..2bb2932c1d 100644 --- a/src/MaaCore/Task/BattleHelper.cpp +++ b/src/MaaCore/Task/BattleHelper.cpp @@ -11,8 +11,8 @@ #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" #include "Utils/NoWarningCV.h" -#include "Vision/Battle/BattleImageAnalyzer.h" #include "Vision/Battle/BattleSkillReadyImageAnalyzer.h" +#include "Vision/Battle/BattlefieldMatcher.h" #include "Vision/BestMatcher.h" #include "Vision/Matcher.h" #include "Vision/RegionOCRer.h" @@ -96,9 +96,10 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable) auto draw_future = std::async(std::launch::async, [&]() { save_map(image); }); } - BattleImageAnalyzer oper_analyzer(image); - oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper); - if (!oper_analyzer.analyze()) { + BattlefieldMatcher oper_analyzer(image); + oper_analyzer.set_object_of_interest({ .deployment = true }); + auto oper_result_opt = oper_analyzer.analyze(); + if (!oper_result_opt) { check_in_battle(image); return false; } @@ -123,7 +124,7 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable) oper.is_unusual_location = battle::get_role_usual_location(oper.role) == oper.location_type; }; - auto cur_opers = oper_analyzer.get_opers(); + auto& cur_opers = oper_result_opt->deployment; std::vector unknown_opers; for (auto& oper : cur_opers) { @@ -224,28 +225,29 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable) bool asst::BattleHelper::update_kills(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; - BattleImageAnalyzer analyzer(image); + BattlefieldMatcher analyzer(image); + analyzer.set_object_of_interest({ .kills = true }); if (m_total_kills) { - analyzer.set_pre_total_kills(m_total_kills); + analyzer.set_total_kills_prompt(m_total_kills); } - analyzer.set_target(BattleImageAnalyzer::Target::Kills); - if (!analyzer.analyze()) { + auto result_opt = analyzer.analyze(); + if (!result_opt || !result_opt->kills) { return false; } - m_kills = analyzer.get_kills(); - m_total_kills = analyzer.get_total_kills(); + std::tie(m_kills, m_total_kills) = result_opt->kills.value(); return true; } bool asst::BattleHelper::update_cost(const cv::Mat& reusable) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; - BattleImageAnalyzer analyzer(image); - analyzer.set_target(BattleImageAnalyzer::Target::Cost); - if (!analyzer.analyze()) { + BattlefieldMatcher analyzer(image); + analyzer.set_object_of_interest({ .costs = true }); + auto result_opt = analyzer.analyze(); + if (!result_opt || !result_opt->costs) { return false; } - m_cost = analyzer.get_cost(); + m_cost = result_opt->costs.value(); return true; } @@ -373,8 +375,10 @@ bool asst::BattleHelper::check_pause_button(const cv::Mat& reusable) Matcher battle_flag_analyzer(image); battle_flag_analyzer.set_task_info("BattleOfficiallyBegin"); bool ret = battle_flag_analyzer.analyze().has_value(); - BattleImageAnalyzer battle_flag_analyzer_2(image); - ret &= battle_flag_analyzer_2.analyze() && battle_flag_analyzer_2.get_pause_button(); + + BattlefieldMatcher battle_flag_analyzer_2(image); + auto battle_result_opt = battle_flag_analyzer_2.analyze(); + ret &= battle_result_opt && battle_result_opt->pause_button; return ret; } @@ -382,8 +386,8 @@ bool asst::BattleHelper::check_in_battle(const cv::Mat& reusable, bool weak) { cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; if (weak) { - BattleImageAnalyzer analyzer(image); - m_in_battle = analyzer.analyze(); + BattlefieldMatcher analyzer(image); + m_in_battle = analyzer.analyze().has_value(); } else { m_in_battle = check_pause_button(image); diff --git a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp index 4f6de94d45..e41f8416e1 100644 --- a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp +++ b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp @@ -9,9 +9,9 @@ #include "Utils/Ranges.hpp" #include "Vision/Battle/BattleDeployDirectionImageAnalyzer.h" #include "Vision/Battle/BattleFormationImageAnalyzer.h" -#include "Vision/Battle/BattleImageAnalyzer.h" #include "Vision/Battle/BattleOperatorsImageAnalyzer.h" #include "Vision/Battle/BattleSkillReadyImageAnalyzer.h" +#include "Vision/Battle/BattlefieldMatcher.h" #include "Vision/BestMatcher.h" #include "Vision/RegionOCRer.h" @@ -193,7 +193,7 @@ bool asst::CombatRecordRecognitionTask::analyze_stage() show_img(stage_analyzer); if (!analyzed) { - // BattleImageAnalyzer battle_analyzer(frame); + // BattlefieldMatcher battle_analyzer(frame); // if (battle_analyzer.analyze()) { // Log.error(i, "already start button, but still failed to analyze stage name"); // m_stage_ocr_end_frame = i; @@ -237,9 +237,10 @@ bool asst::CombatRecordRecognitionTask::analyze_deployment() const int skip_count = m_video_fps > m_deployment_fps ? static_cast(m_video_fps / m_deployment_fps) - 1 : 0; - BattleImageAnalyzer oper_analyzer; - oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper); + BattlefieldMatcher oper_analyzer; + oper_analyzer.set_object_of_interest({ .deployment = true }); + std::vector deployment; for (size_t i = m_stage_ocr_end_frame; i < m_video_frame_count; i += skip_frames(skip_count) + 1) { cv::Mat frame; *m_video_ptr >> frame; @@ -252,14 +253,15 @@ bool asst::CombatRecordRecognitionTask::analyze_deployment() cv::resize(frame, frame, cv::Size(), m_scale, m_scale, cv::INTER_AREA); oper_analyzer.set_image(frame); - bool analyzed = oper_analyzer.analyze() && oper_analyzer.get_pause_button(); + auto oper_result_opt = oper_analyzer.analyze(); + bool analyzed = oper_result_opt && oper_result_opt->pause_button; show_img(oper_analyzer); if (analyzed) { m_battle_start_frame = i; + deployment = std::move(oper_result_opt->deployment); break; } } - const auto& deployment = oper_analyzer.get_opers(); auto avatar_task_ptr = Task.get("BattleAvatarDataForFormation"); for (const auto& [name, formation_avatar] : m_formation) { @@ -352,14 +354,18 @@ bool asst::CombatRecordRecognitionTask::slice_video() } cv::resize(frame, frame, cv::Size(), m_scale, m_scale, cv::INTER_AREA); - BattleImageAnalyzer oper_analyzer(frame); - oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper | BattleImageAnalyzer::Target::DetailPage | - BattleImageAnalyzer::Target::Kills); - oper_analyzer.set_pre_total_kills(total_kills); - bool analyzed = oper_analyzer.analyze(); - show_img(oper_analyzer); + BattlefieldMatcher analyzer(frame); + analyzer.set_object_of_interest({ + .deployment = true, + .kills = true, + .in_detail = true, + }); - if (!analyzed) { + analyzer.set_total_kills_prompt(total_kills); + auto result_opt = analyzer.analyze(); + show_img(analyzer); + + if (!result_opt) { battle_over(); if (++not_in_battle_count > 10) { break; @@ -369,14 +375,16 @@ bool asst::CombatRecordRecognitionTask::slice_video() m_battle_end_frame = 0; not_in_battle_count = 0; - int cur_kills = oper_analyzer.get_kills(); - if (cur_kills != latest_kills) { - m_frame_kills.emplace_back(std::make_pair(i, cur_kills)); + if (result_opt->kills) { + auto& [cur_kills, cur_total_kills] = *result_opt->kills; + if (cur_kills != latest_kills) { + m_frame_kills.emplace_back(std::make_pair(i, cur_kills)); + } + latest_kills = cur_kills; + total_kills = cur_total_kills; } - total_kills = oper_analyzer.get_total_kills(); - latest_kills = oper_analyzer.get_kills(); - const auto& cur_opers = oper_analyzer.get_opers(); + const auto& cur_opers = result_opt->deployment; bool continuity = true; int pre_distance = 0; for (auto iter = cur_opers.begin(); iter != cur_opers.end(); ++iter) { @@ -390,7 +398,7 @@ bool asst::CombatRecordRecognitionTask::slice_video() pre_distance = distance; } - bool in_detail_page = oper_analyzer.get_in_detail_page() || !oper_analyzer.get_pause_button() || !continuity; + bool in_detail_page = result_opt->in_detail || !result_opt->pause_button || !continuity; bool oper_auto_retreat = in_segment && continuity && !m_clips.empty() && cur_opers.size() != m_clips.back().deployment.size(); @@ -821,22 +829,22 @@ json::object asst::CombatRecordRecognitionTask::analyze_action_condition(ClipInf { "kills", kills }, }; - BattleImageAnalyzer analyzer; - analyzer.set_target(BattleImageAnalyzer::Target::Cost); - - analyzer.set_image(pre_clip_ptr->end_frame); // 开始执行这次操作的画面 - if (!analyzer.analyze()) { + BattlefieldMatcher analyzer(pre_clip_ptr->end_frame); // 开始执行这次操作的画面 + analyzer.set_object_of_interest({ .costs = true }); + auto end_result_opt = analyzer.analyze(); + if (!end_result_opt || !end_result_opt->costs) { m_pre_action_costs = -1; return condition; } - int start_costs = analyzer.get_cost(); + int start_costs = end_result_opt->costs.value(); int cost_changes = start_costs - m_pre_action_costs; if (m_pre_action_costs >= 0 && cost_changes > 0) { condition.emplace("cost_changes", cost_changes); } analyzer.set_image(clip.start_frame); // 这次操作执行完了的画面 - m_pre_action_costs = analyzer.analyze() ? analyzer.get_cost() : start_costs; + auto start_result_opt = analyzer.analyze(); + m_pre_action_costs = (start_result_opt && start_result_opt->costs) ? start_result_opt->costs.value() : start_costs; return condition; } diff --git a/src/MaaCore/Task/Interface/DebugTask.cpp b/src/MaaCore/Task/Interface/DebugTask.cpp index 207fceed0f..46990e6a1c 100644 --- a/src/MaaCore/Task/Interface/DebugTask.cpp +++ b/src/MaaCore/Task/Interface/DebugTask.cpp @@ -6,8 +6,8 @@ #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" -#include "Vision/Battle/BattleImageAnalyzer.h" #include "Vision/Battle/BattleSkillReadyImageAnalyzer.h" +#include "Vision/Battle/BattlefieldMatcher.h" #include "Vision/Miscellaneous/DepotImageAnalyzer.h" #include "Vision/Miscellaneous/StageDropsImageAnalyzer.h" @@ -15,8 +15,6 @@ asst::DebugTask::DebugTask(const AsstCallback& callback, Assistant* inst) : Inte bool asst::DebugTask::run() { - test_battle_image(); - return true; } @@ -66,7 +64,7 @@ void asst::DebugTask::test_battle_image() cv::Mat image = asst::imread(utils::path("1.png")); cv::Mat resized; cv::resize(image, resized, cv::Size(1280, 720), 0, 0, cv::INTER_AREA); - BattleImageAnalyzer analyzer(resized); - analyzer.set_target(BattleImageAnalyzer::Target::Oper); + BattlefieldMatcher analyzer(resized); + analyzer.set_object_of_interest({ .deployment = true }); analyzer.analyze(); } diff --git a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp index a6e69d5d87..7eb17d0cd9 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp @@ -16,8 +16,6 @@ #include "Utils/Algorithm.hpp" #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" -#include "Vision/Battle/BattleImageAnalyzer.h" -#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h" #include "Vision/Matcher.h" #include "Vision/RegionOCRer.h" diff --git a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h index 1a41f0c337..cde77e8a95 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h +++ b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h @@ -5,7 +5,6 @@ #include "Common/AsstBattleDef.h" #include "Common/AsstTypes.h" #include "Config/Miscellaneous/TilePack.h" -#include "Vision/Battle/BattleImageAnalyzer.h" namespace asst { diff --git a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp index 864a71e9c2..5024b5076b 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp @@ -17,8 +17,6 @@ #include "Task/ProcessTask.h" #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" -#include "Vision/Battle/BattleImageAnalyzer.h" -#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h" #include "Vision/Matcher.h" #include "Vision/RegionOCRer.h" diff --git a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h index e08b17516e..4dbf88f451 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h +++ b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h @@ -8,7 +8,6 @@ #include "Config/Miscellaneous/TilePack.h" #include "Task/AbstractTaskPlugin.h" #include "Task/BattleHelper.h" -#include "Vision/Battle/BattleImageAnalyzer.h" namespace asst { diff --git a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp b/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp deleted file mode 100644 index 6b87a9c455..0000000000 --- a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp +++ /dev/null @@ -1,489 +0,0 @@ -#include "BattleImageAnalyzer.h" - -#include "Utils/Ranges.hpp" -#include - -#include "Utils/NoWarningCV.h" - -#include "Config/TaskData.h" -#include "Config/TemplResource.h" -#include "Utils/Logger.hpp" -#include "Vision/BestMatcher.h" -#include "Vision/Matcher.h" -#include "Vision/MultiMatcher.h" -#include "Vision/TemplDetOCRer.h" -#include "Vision/RegionOCRer.h" - -bool asst::BattleImageAnalyzer::set_target(int target) -{ - m_target = target; - return true; -} - -void asst::BattleImageAnalyzer::set_pre_total_kills(int pre_total_kills) -{ - m_pre_total_kills = pre_total_kills; -} - -bool asst::BattleImageAnalyzer::analyze() -{ - clear(); - - // flag 无论如何都识别。表明当前画面是在战斗场景的 - bool ret = flag_analyze(); - if (!ret) { - return false; - } - - // - // if (m_target & Target::Home) { - // ret |= home_analyze(); - //} - - // 可能没有干员(全上场了),所以干员识别结果不影响返回值 - if (m_target & Target::Oper) { - opers_analyze(); - } - - if (m_target & Target::DetailPage) { - detail_page_analyze(); - } - - if (m_target & Target::Kills) { - ret &= kills_analyze(); - } - - if (m_target & Target::Cost) { - ret &= cost_analyze(); - } - - // if (m_target & Target::Vacancies) { - // ret &= vacancies_analyze(); - // } - - return ret; -} - -const std::vector& asst::BattleImageAnalyzer::get_opers() const noexcept -{ - return m_opers; -} - -const std::vector& asst::BattleImageAnalyzer::get_homes() const noexcept -{ - return m_homes; -} - -int asst::BattleImageAnalyzer::get_hp() const noexcept -{ - return m_hp; -} - -int asst::BattleImageAnalyzer::get_kills() const noexcept -{ - return m_kills; -} - -int asst::BattleImageAnalyzer::get_total_kills() const noexcept -{ - return m_total_kills; -} - -int asst::BattleImageAnalyzer::get_cost() const noexcept -{ - return m_cost; -} - -bool asst::BattleImageAnalyzer::get_in_detail_page() const noexcept -{ - return m_in_detail_page; -} - -bool asst::BattleImageAnalyzer::get_pause_button() const noexcept -{ - return m_pause_button; -} - -void asst::BattleImageAnalyzer::clear() noexcept -{ - m_opers.clear(); - m_homes.clear(); - m_ready_skills.clear(); - m_hp = 0; - m_kills = 0; - m_cost = 0; -} - -void asst::BattleImageAnalyzer::sort_opers_by_cost() -{ - // 本来游戏就是按费用排的,这里倒序一下就行了 - ranges::reverse(m_opers); -} - -bool asst::BattleImageAnalyzer::opers_analyze() -{ - MultiMatcher flags_analyzer(m_image); - const auto& flag_task_ptr = Task.get("BattleOpersFlag"); - flags_analyzer.set_task_info(flag_task_ptr); -#ifndef ASST_DEBUG - flags_analyzer.set_log_tracing(false); -#endif - if (!flags_analyzer.analyze()) { - return false; - } - auto flags = flags_analyzer.get_result(); - if (flags.empty()) { - return false; - } - sort_by_horizontal_(flags); - - const auto click_move = Task.get("BattleOperClickRange")->rect_move; - const auto role_move = Task.get("BattleOperRoleRange")->rect_move; - // const auto cost_move = Task.get("BattleOperCostRange")->rect_move; - const auto avlb_move = Task.get("BattleOperAvailable")->rect_move; - const auto cooling_move = Task.get("BattleOperCooling")->rect_move; - const auto avatar_move = Task.get("BattleOperAvatar")->rect_move; - // const int unselected_y = flag_task_ptr->roi.y; - - size_t index = 0; - for (const auto& flag_mrect : flags) { - battle::DeploymentOper oper; - oper.rect = flag_mrect.rect.move(click_move); - - Rect role_rect = flag_mrect.rect.move(role_move); - oper.role = oper_role_analyze(role_rect); - if (oper.role == battle::Role::Unknown) { - Log.warn("Unknown role"); - continue; - } - - if (oper.rect.x + oper.rect.width >= m_image.cols) { - oper.rect.width = m_image.cols - oper.rect.x; - } - Rect avatar_rect = oper.rect.move(avatar_move); - oper.avatar = m_image(make_rect(avatar_rect)); - - Rect available_rect = flag_mrect.rect.move(avlb_move); - oper.available = oper_available_analyze(available_rect); - -#ifdef ASST_DEBUG - if (oper.available) { - cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 255, 0), 2); - } - else { - cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 0, 255), 2); - } -#endif - - Rect cooling_rect = flag_mrect.rect.move(cooling_move); - if (cooling_rect.x + cooling_rect.width >= m_image.cols) { - cooling_rect.width = m_image.cols - cooling_rect.x; - } - if (cooling_rect.y + cooling_rect.height >= m_image.rows) { - cooling_rect.height = m_image.rows - cooling_rect.y; - } - oper.cooling = oper_cooling_analyze(cooling_rect); - if (oper.cooling && oper.available) { - Log.error("oper is available, but with cooling"); - } - - // 干员费用识别的不太准,暂时也没用上,先注释掉,TODO:优化费用识别 - // Rect cost_rect = flag_mrect.rect.move(cost_move); - // oper.cost = oper_cost_analyze(cost_rect); - oper.index = index++; - // oper.selected = flag_mrect.rect.y < unselected_y; - - m_opers.emplace_back(std::move(oper)); - } - - return true; -} - -asst::battle::Role asst::BattleImageAnalyzer::oper_role_analyze(const Rect& roi) -{ - static const std::unordered_map RoleMap = { - { "Caster", battle::Role::Caster }, { "Medic", battle::Role::Medic }, { "Pioneer", battle::Role::Pioneer }, - { "Sniper", battle::Role::Sniper }, { "Special", battle::Role::Special }, { "Support", battle::Role::Support }, - { "Tank", battle::Role::Tank }, { "Warrior", battle::Role::Warrior }, { "Drone", battle::Role::Drone }, - }; - - static const std::string TaskName = "BattleOperRole"; - static const std::string Ext = ".png"; - BestMatcher role_analyzer(m_image); -#ifndef ASST_DEBUG - role_analyzer.set_log_tracing(false); -#endif // !ASST_DEBUG - role_analyzer.set_task_info(TaskName); - role_analyzer.set_roi(roi); - - for (const auto& role_name : RoleMap | views::keys) { - role_analyzer.append_templ(TaskName + role_name + Ext); - } - if (!role_analyzer.analyze()) { - Log.warn(__FUNCTION__, "unknown role"); - return battle::Role::Unknown; - } - - const auto& templ_name = role_analyzer.get_result().templ_info.name; - - std::string role_name = templ_name.substr(TaskName.size(), templ_name.size() - TaskName.size() - Ext.size()); - -#ifdef ASST_DEBUG - cv::putText(m_image_draw, role_name, cv::Point(roi.x, roi.y - 5), 1, 1, cv::Scalar(0, 255, 255)); -#endif - - return RoleMap.at(role_name); -} - -bool asst::BattleImageAnalyzer::oper_cooling_analyze(const Rect& roi) -{ - const auto cooling_task_ptr = Task.get("BattleOperCooling"); - - auto img_roi = m_image(make_rect(roi)); - cv::Mat hsv; - cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV); - int h_low = cooling_task_ptr->mask_range.first; - int h_up = cooling_task_ptr->mask_range.second; - int s_low = cooling_task_ptr->specific_rect.x; - int s_up = cooling_task_ptr->specific_rect.y; - int v_low = cooling_task_ptr->specific_rect.width; - int v_up = cooling_task_ptr->specific_rect.height; - - cv::Mat bin; - cv::inRange(hsv, cv::Scalar(h_low, s_low, v_low), cv::Scalar(h_up, s_up, v_up), bin); - - int count = 0; - for (int i = 0; i != bin.rows; ++i) { - for (int j = 0; j != bin.cols; ++j) { - cv::uint8_t value = bin.at(i, j); - if (value) { - ++count; - } - } - } - // Log.trace("oper_cooling_analyze |", count); - return count >= cooling_task_ptr->special_params.front(); -} - -int asst::BattleImageAnalyzer::oper_cost_analyze(const Rect& roi) -{ - std::ignore = roi; - return 0; -} - -bool asst::BattleImageAnalyzer::oper_available_analyze(const Rect& roi) -{ - cv::Mat hsv; - cv::cvtColor(m_image(make_rect(roi)), hsv, cv::COLOR_BGR2HSV); - cv::Scalar avg = cv::mean(hsv); - // Log.trace("oper available, mean", avg[2]); - - const int thres = Task.get("BattleOperAvailable")->special_params.front(); - if (avg[2] < thres) { - return false; - } - return true; -} - -bool asst::BattleImageAnalyzer::home_analyze() -{ - // 颜色转换 - cv::Mat hsv; - cv::cvtColor(m_image, hsv, cv::COLOR_BGR2HSV); - cv::Mat bin; - cv::inRange(hsv, cv::Scalar(104, 160, 180), cv::Scalar(107, 220, 255), bin); - - // 开操作降噪 - cv::Mat morph_dst; - cv::Mat open_kernel = cv::getStructuringElement(cv::MORPH_CROSS, cv::Size(1, 1)); - cv::morphologyEx(bin, morph_dst, cv::MORPH_OPEN, open_kernel); - - // 霍夫线 - std::vector lines; - cv::HoughLinesP(morph_dst, lines, 1, 60 * CV_PI / 180, 10, 20, 10); - - int left = INT_MAX, right = 0, top = INT_MAX, bottom = 0; - for (auto&& l : lines) { - left = (std::min)({ left, l[0], l[2] }); - right = (std::max)({ right, l[0], l[2] }); - top = (std::min)({ top, l[1], l[3] }); - bottom = (std::max)({ bottom, l[1], l[3] }); - -#ifdef ASST_DEBUG - cv::line(m_image_draw, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(0, 0, 255), 3); -#endif - } - if (right == 0) { - Log.error("home recognition error"); - return false; - } - Rect home_rect(left, top, right - left, bottom - top); - m_homes.emplace_back(home_rect); - -#ifdef ASST_DEBUG - cv::rectangle(m_image_draw, make_rect(home_rect), cv::Scalar(0, 255, 0), 5); -#endif - - return true; -} - -bool asst::BattleImageAnalyzer::hp_flag_analyze() -{ - // 识别 HP 的那个蓝白色图标 - auto flag_task_ptr = Task.get("BattleHpFlag"); - Matcher flag_analyzer(m_image); - flag_analyzer.set_task_info(flag_task_ptr); - if (!flag_analyzer.analyze()) { - // 漏怪的时候,那个图标会变成红色的,所以多识别一次 - flag_task_ptr = Task.get("BattleHpFlag2"); - flag_analyzer.set_task_info(flag_task_ptr); - if (!flag_analyzer.analyze()) { - return false; - } - } - return true; -} - -bool asst::BattleImageAnalyzer::kills_flag_analyze() -{ - Matcher flag_analyzer(m_image); - flag_analyzer.set_task_info("BattleKillsFlag"); - return flag_analyzer.analyze().has_value(); -} - -bool asst::BattleImageAnalyzer::kills_analyze() -{ - TemplDetOCRer kills_analyzer(m_image); - kills_analyzer.set_task_info("BattleKillsFlag", "BattleKills"); - kills_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map); - - if (!kills_analyzer.analyze()) { - return false; - } - - std::string kills_text = kills_analyzer.get_result().front().text; - if (kills_text.empty()) { - return false; - } - - size_t pos = kills_text.find('/'); - if (pos == std::string::npos) { - Log.warn("cannot found flag /"); - // 这种时候绝大多数是把 "0/41" 中的 '/' 识别成了别的什么东西(其中又有绝大部分情况是识别成了 '1') - // 所以这里依赖 m_pre_total_kills 转一下 - if (m_pre_total_kills <= 0) { - // 第一次识别就识别错了,识别成了 "0141" - if (kills_text.at(0) == '0') { - pos = 1; - } - else { - Log.error("m_pre_total_kills is zero"); - return false; - } - } - else { - size_t pre_pos = kills_text.find(std::to_string(m_pre_total_kills)); - if (pre_pos == std::string::npos || pre_pos == 0) { - Log.error("can't get pre_pos"); - return false; - } - Log.trace("pre total kills pos:", pre_pos); - pos = pre_pos - 1; - } - } - - // 例子中的"0" - std::string kills_count = kills_text.substr(0, pos); - if (kills_count.empty() || !ranges::all_of(kills_count, [](char c) -> bool { return std::isdigit(c); })) { - return false; - } - int cur_kills = std::stoi(kills_count); - m_kills = std::max(m_kills, cur_kills); - - // 例子中的"41" - std::string total_kills = kills_text.substr(pos + 1, std::string::npos); - int cur_total_kills = 0; - if (total_kills.empty() || !ranges::all_of(total_kills, [](char c) -> bool { return std::isdigit(c); })) { - Log.warn("total kills recognition failed, set to", m_pre_total_kills); - cur_total_kills = m_pre_total_kills; - } - else { - cur_total_kills = std::stoi(total_kills); - } - m_total_kills = std::max(cur_total_kills, m_pre_total_kills); - - Log.trace("Kills:", m_kills, "/", m_total_kills); - return m_kills <= m_total_kills; -} - -bool asst::BattleImageAnalyzer::cost_analyze() -{ - RegionOCRer cost_analyzer(m_image); - cost_analyzer.set_task_info("BattleCostData"); - cost_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map); - - if (!cost_analyzer.analyze()) { - return false; - } - - std::string cost_str = cost_analyzer.get_result().text; - - if (cost_str.empty() || !ranges::all_of(cost_str, [](const char& c) -> bool { return std::isdigit(c); })) { - return false; - } - m_cost = std::stoi(cost_str); - - return true; -} - -bool asst::BattleImageAnalyzer::vacancies_analyze() -{ - return false; -} - -bool asst::BattleImageAnalyzer::flag_analyze() -{ - return pause_button_analyze() || hp_flag_analyze() || kills_flag_analyze(); -} - -bool asst::BattleImageAnalyzer::pause_button_analyze() -{ - auto has_started_task_ptr = Task.get("BattleHasStarted"); - cv::Mat roi = m_image(make_rect(has_started_task_ptr->roi)); - cv::Mat roi_gray; - cv::cvtColor(roi, roi_gray, cv::COLOR_BGR2GRAY); - cv::Mat bin; - const int value_threshold = has_started_task_ptr->special_params[0]; - cv::threshold(roi_gray, bin, value_threshold, 255, cv::THRESH_BINARY); - int count = cv::countNonZero(bin); - const int count_threshold = has_started_task_ptr->special_params[1]; - Log.trace(__FUNCTION__, "count", count, "threshold", count_threshold); - - m_pause_button = count > count_threshold; - return m_pause_button; -} - -bool asst::BattleImageAnalyzer::detail_page_analyze() -{ - auto analyze = [&](const std::string& task_name) { - auto task_ptr = Task.get(task_name); - cv::Mat roi = m_image(make_rect(task_ptr->roi)); - cv::Mat roi_hsv; - cv::cvtColor(roi, roi_hsv, cv::COLOR_BGR2HSV); - cv::Mat bin1; - cv::inRange(roi_hsv, cv::Scalar(99, 235, 235), cv::Scalar(105, 255, 255), bin1); - int count1 = cv::countNonZero(bin1); - - cv::Mat bin2; - cv::inRange(roi_hsv, cv::Scalar(99, 235, 135), cv::Scalar(105, 255, 155), bin2); - int count2 = cv::countNonZero(bin2); - - const int threshold = task_ptr->special_params[0]; - Log.info("detail_page, count:", count1, count2, ", threshold:", threshold); - - return count1 > threshold || count2 > threshold; - }; - - m_in_detail_page = analyze("BattleOperDetailPageFlag") || analyze("BattleOperDetailPageOldFlag"); - return m_in_detail_page; -} diff --git a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h b/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h deleted file mode 100644 index 7bca839a43..0000000000 --- a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once -#include "Vision/VisionHelper.h" - -#include "Common/AsstBattleDef.h" - -namespace asst -{ - class BattleImageAnalyzer : public VisionHelper - { - public: - enum Target // 需要识别的目标 - { - None = 0, - Oper = 1, // 下方的干员信息 - DetailPage = 2, // 是否点开了详情页 - Kills = 4, // 击杀数 - Cost = 8, // 费用 - }; - - public: - using VisionHelper::VisionHelper; - virtual ~BattleImageAnalyzer() override = default; - - bool set_target(int target); - void set_pre_total_kills(int pre_total_kills); - bool analyze(); - - virtual const std::vector& get_opers() const noexcept; - virtual const std::vector& get_homes() const noexcept; - - int get_hp() const noexcept; - int get_kills() const noexcept; - int get_total_kills() const noexcept; - int get_cost() const noexcept; - bool get_in_detail_page() const noexcept; - bool get_pause_button() const noexcept; - - void clear() noexcept; - void sort_opers_by_cost(); // 高费在前,费用降序 - - protected: - bool opers_analyze(); // 识别干员 - bool detail_page_analyze(); // 识别是否在详情页 - battle::Role oper_role_analyze(const Rect& roi); - bool oper_cooling_analyze(const Rect& roi); - int oper_cost_analyze(const Rect& roi); - bool oper_available_analyze(const Rect& roi); - - bool home_analyze(); // 识别蓝色的家门 - // bool skill_analyze(); // 识别技能是否可用 - bool kills_analyze(); // 识别击杀数 - bool cost_analyze(); // 识别费用 - bool vacancies_analyze(); // 识别剩余可部署人数 - bool flag_analyze(); - bool hp_flag_analyze(); - bool kills_flag_analyze(); - bool pause_button_analyze(); - - int m_target = 0; // 待识别的目标 - int m_pre_total_kills = 0; // 之前的击杀总数,因为击杀数经常识别不准所以依赖外部传入作为参考 - - /* 识别结果缓存 */ - std::vector m_opers; // 下方干员信息 - std::vector m_homes; // 蓝色的家门位置 - std::vector m_ready_skills; // 可以释放的技能(Rect实际是干员的位置) - int m_hp = 0; // 剩余生命值 - int m_kills = 0; // 击杀数 - int m_total_kills = 0; // 击杀总数 - int m_cost = 0; // 部署费用 - bool m_in_detail_page = false; - bool m_pause_button = false; - }; -} // namespace asst diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp new file mode 100644 index 0000000000..f8c6cd32ca --- /dev/null +++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp @@ -0,0 +1,360 @@ +#include "BattlefieldMatcher.h" + +#include "Utils/Ranges.hpp" +#include + +#include "Utils/NoWarningCV.h" + +#include "Config/TaskData.h" +#include "Config/TemplResource.h" +#include "Utils/Logger.hpp" +#include "Vision/BestMatcher.h" +#include "Vision/Matcher.h" +#include "Vision/MultiMatcher.h" +#include "Vision/RegionOCRer.h" +#include "Vision/TemplDetOCRer.h" + +using namespace asst; + +void BattlefieldMatcher::set_object_of_interest(ObjectOfInterest obj) +{ + m_object_of_interest = std::move(obj); +} + +void BattlefieldMatcher::set_total_kills_prompt(int prompt) +{ + m_total_kills_prompt = prompt; +} + +BattlefieldMatcher::ResultOpt BattlefieldMatcher::analyze() const +{ + Result result; + + if (m_object_of_interest.flag) { + result.pause_button = pause_button_analyze(); + if (!result.pause_button && !hp_flag_analyze() && !kills_flag_analyze()) { + // flag 表明当前画面是在战斗场景的,不在的就没必要识别了 + return std::nullopt; + } + } + + if (m_object_of_interest.deployment) { + result.deployment = deployment_analyze(); + } + + if (m_object_of_interest.kills) { + result.kills = kills_analyze(); + if (!result.kills) { + return std::nullopt; + } + } + + if (m_object_of_interest.costs) { + result.costs = costs_analyze(); + if (!result.costs) { + return std::nullopt; + } + } + + if (m_object_of_interest.in_detail) { + result.in_detail = in_detail_analyze(); + } + + return result; +} + +std::vector BattlefieldMatcher::deployment_analyze() const +{ + MultiMatcher flags_analyzer(m_image); + const auto& flag_task_ptr = Task.get("BattleOpersFlag"); + flags_analyzer.set_task_info(flag_task_ptr); + +#ifndef ASST_DEBUG + flags_analyzer.set_log_tracing(false); +#endif + + auto flag_opt = flags_analyzer.analyze(); + if (!flag_opt) { + return {}; + } + auto& flags = flag_opt.value(); + sort_by_horizontal_(flags); + + const Rect& click_move = Task.get("BattleOperClickRange")->rect_move; + const Rect& role_move = Task.get("BattleOperRoleRange")->rect_move; + const Rect& avlb_move = Task.get("BattleOperAvailable")->rect_move; + const Rect& cooling_move = Task.get("BattleOperCooling")->rect_move; + const Rect& avatar_move = Task.get("BattleOperAvatar")->rect_move; + + std::vector oper_result; + size_t index = 0; + for (const auto& flag_res : flags) { + battle::DeploymentOper oper; + oper.rect = flag_res.rect.move(click_move); + + Rect role_rect = flag_res.rect.move(role_move); + oper.role = oper_role_analyze(role_rect); + if (oper.role == battle::Role::Unknown) { + Log.warn("Unknown role"); + continue; + } + + if (oper.rect.x + oper.rect.width >= m_image.cols) { + oper.rect.width = m_image.cols - oper.rect.x; + } + Rect avatar_rect = oper.rect.move(avatar_move); + oper.avatar = m_image(make_rect(avatar_rect)); + + Rect available_rect = flag_res.rect.move(avlb_move); + oper.available = oper_available_analyze(available_rect); + +#ifdef ASST_DEBUG + if (oper.available) { + cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 255, 0), 2); + } + else { + cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 0, 255), 2); + } +#endif + + Rect cooling_rect = correct_rect(flag_res.rect.move(cooling_move), m_image); + oper.cooling = oper_cooling_analyze(cooling_rect); + if (oper.cooling && oper.available) { + Log.error("oper is available, but with cooling"); + } + + oper.index = index++; + + oper_result.emplace_back(std::move(oper)); + } + + return oper_result; +} + +battle::Role BattlefieldMatcher::oper_role_analyze(const Rect& roi) const +{ + static const std::unordered_map RoleMap = { + { "Caster", battle::Role::Caster }, { "Medic", battle::Role::Medic }, { "Pioneer", battle::Role::Pioneer }, + { "Sniper", battle::Role::Sniper }, { "Special", battle::Role::Special }, { "Support", battle::Role::Support }, + { "Tank", battle::Role::Tank }, { "Warrior", battle::Role::Warrior }, { "Drone", battle::Role::Drone }, + }; + + static const std::string TaskName = "BattleOperRole"; + static const std::string Ext = ".png"; + BestMatcher role_analyzer(m_image); +#ifndef ASST_DEBUG + role_analyzer.set_log_tracing(false); +#endif // !ASST_DEBUG + role_analyzer.set_task_info(TaskName); + role_analyzer.set_roi(roi); + + for (const auto& role_name : RoleMap | views::keys) { + role_analyzer.append_templ(TaskName + role_name + Ext); + } + auto role_opt = role_analyzer.analyze(); + if (!role_opt) { + Log.warn(__FUNCTION__, "unknown role"); + return battle::Role::Unknown; + } + + const auto& templ_name = role_opt->templ_info.name; + + std::string role_name = templ_name.substr(TaskName.size(), templ_name.size() - TaskName.size() - Ext.size()); + +#ifdef ASST_DEBUG + cv::putText(m_image_draw, role_name, cv::Point(roi.x, roi.y - 5), 1, 1, cv::Scalar(0, 255, 255)); +#endif + + return RoleMap.at(role_name); +} + +bool BattlefieldMatcher::oper_cooling_analyze(const Rect& roi) const +{ + const auto cooling_task_ptr = Task.get("BattleOperCooling"); + + auto img_roi = m_image(make_rect(roi)); + cv::Mat hsv; + cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV); + int h_low = cooling_task_ptr->mask_range.first; + int h_up = cooling_task_ptr->mask_range.second; + int s_low = cooling_task_ptr->specific_rect.x; + int s_up = cooling_task_ptr->specific_rect.y; + int v_low = cooling_task_ptr->specific_rect.width; + int v_up = cooling_task_ptr->specific_rect.height; + + cv::Mat bin; + cv::inRange(hsv, cv::Scalar(h_low, s_low, v_low), cv::Scalar(h_up, s_up, v_up), bin); + + int count = 0; + for (int i = 0; i != bin.rows; ++i) { + for (int j = 0; j != bin.cols; ++j) { + cv::uint8_t value = bin.at(i, j); + if (value) { + ++count; + } + } + } + // Log.trace("oper_cooling_analyze |", count); + return count >= cooling_task_ptr->special_params.front(); +} + +int BattlefieldMatcher::oper_cost_analyze(const Rect& roi) const +{ + std::ignore = roi; + return 0; +} + +bool BattlefieldMatcher::oper_available_analyze(const Rect& roi) const +{ + cv::Mat hsv; + cv::cvtColor(m_image(make_rect(roi)), hsv, cv::COLOR_BGR2HSV); + cv::Scalar avg = cv::mean(hsv); + // Log.trace("oper available, mean", avg[2]); + + const int thres = Task.get("BattleOperAvailable")->special_params.front(); + if (avg[2] < thres) { + return false; + } + return true; +} + +bool BattlefieldMatcher::hp_flag_analyze() const +{ + // 识别 HP 的那个蓝白色图标 + Matcher flag_analyzer(m_image); + flag_analyzer.set_task_info("BattleHpFlag"); + if (flag_analyzer.analyze()) { + return true; + } + + // 漏怪的时候,那个图标会变成红色的,所以多识别一次 + flag_analyzer.set_task_info("BattleHpFlag2"); + return flag_analyzer.analyze().has_value(); +} + +bool BattlefieldMatcher::kills_flag_analyze() const +{ + Matcher flag_analyzer(m_image); + flag_analyzer.set_task_info("BattleKillsFlag"); + return flag_analyzer.analyze().has_value(); +} + +std::optional> BattlefieldMatcher::kills_analyze() const +{ + TemplDetOCRer kills_analyzer(m_image); + kills_analyzer.set_task_info("BattleKillsFlag", "BattleKills"); + kills_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map); + + auto kills_opt = kills_analyzer.analyze(); + if (!kills_opt) { + return std::nullopt; + } + const std::string& kills_text = kills_opt->front().text; + + size_t pos = kills_text.find('/'); + if (pos == std::string::npos) { + Log.warn("cannot found flag /"); + // 这种时候绝大多数是把 "0/41" 中的 '/' 识别成了别的什么东西(其中又有绝大部分情况是识别成了 '1') + // 所以这里依赖 m_pre_total_kills 转一下 + if (m_total_kills_prompt <= 0) { + // 第一次识别就识别错了,识别成了 "0141" + if (kills_text.at(0) != '0') { + Log.error("m_total_kills_prompt is zero"); + return std::nullopt; + } + pos = 1; + } + else { + size_t pre_pos = kills_text.find(std::to_string(m_total_kills_prompt)); + if (pre_pos == std::string::npos || pre_pos == 0) { + Log.error("can't get pre_pos"); + return std::nullopt; + } + Log.trace("pre total kills pos:", pre_pos); + pos = pre_pos - 1; + } + } + + // 例子中的"0" + std::string kills_count = kills_text.substr(0, pos); + if (kills_count.empty() || !ranges::all_of(kills_count, [](char c) -> bool { return std::isdigit(c); })) { + return std::nullopt; + } + int kills = std::stoi(kills_count); + + // 例子中的"41" + std::string total_kills_text = kills_text.substr(pos + 1, std::string::npos); + int total_kills = 0; + if (total_kills_text.empty() || !ranges::all_of(total_kills_text, [](char c) -> bool { return std::isdigit(c); })) { + Log.warn("total kills recognition failed, set to", m_total_kills_prompt); + total_kills = m_total_kills_prompt; + } + else { + total_kills = std::stoi(total_kills_text); + } + total_kills = std::max(total_kills, m_total_kills_prompt); + + Log.trace("Kills:", kills, "/", total_kills); + return std::make_pair(kills, total_kills); +} + +std::optional BattlefieldMatcher::costs_analyze() const +{ + RegionOCRer cost_analyzer(m_image); + cost_analyzer.set_task_info("BattleCostData"); + cost_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map); + + auto cost_opt = cost_analyzer.analyze(); + if (!cost_opt) { + return std::nullopt; + } + const std::string& cost_str = cost_opt->text; + + if (cost_str.empty() || !ranges::all_of(cost_str, [](const char& c) -> bool { return std::isdigit(c); })) { + return std::nullopt; + } + return std::stoi(cost_str); +} + +bool BattlefieldMatcher::pause_button_analyze() const +{ + auto has_started_task_ptr = Task.get("BattleHasStarted"); + cv::Mat roi = m_image(make_rect(has_started_task_ptr->roi)); + cv::Mat roi_gray; + cv::cvtColor(roi, roi_gray, cv::COLOR_BGR2GRAY); + cv::Mat bin; + const int value_threshold = has_started_task_ptr->special_params[0]; + cv::threshold(roi_gray, bin, value_threshold, 255, cv::THRESH_BINARY); + int count = cv::countNonZero(bin); + const int count_threshold = has_started_task_ptr->special_params[1]; + Log.trace(__FUNCTION__, "count", count, "threshold", count_threshold); + + return count > count_threshold; +} + +bool BattlefieldMatcher::in_detail_analyze() const +{ + auto analyze = [&](const std::string& task_name) { + auto task_ptr = Task.get(task_name); + cv::Mat roi = m_image(make_rect(task_ptr->roi)); + cv::Mat roi_hsv; + cv::cvtColor(roi, roi_hsv, cv::COLOR_BGR2HSV); + cv::Mat bin1; + cv::inRange(roi_hsv, cv::Scalar(99, 235, 235), cv::Scalar(105, 255, 255), bin1); + int count1 = cv::countNonZero(bin1); + + cv::Mat bin2; + cv::inRange(roi_hsv, cv::Scalar(99, 235, 135), cv::Scalar(105, 255, 155), bin2); + int count2 = cv::countNonZero(bin2); + + const int threshold = task_ptr->special_params[0]; + Log.info("in_detail, count:", count1, count2, ", threshold:", threshold); + + return count1 > threshold || count2 > threshold; + }; + + if (!analyze("BattleOperDetailPageFlag") && !analyze("BattleOperDetailPageOldFlag")) { + return false; + } + return true; +} diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.h b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h new file mode 100644 index 0000000000..18a02866a1 --- /dev/null +++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h @@ -0,0 +1,62 @@ +#pragma once +#include "Vision/VisionHelper.h" + +#include "Common/AsstBattleDef.h" + +namespace asst +{ + class BattlefieldMatcher : public VisionHelper + { + public: + struct ObjectOfInterest + { + bool flag = true; + bool deployment = false; + bool kills = false; + bool costs = false; + bool in_detail = false; + }; + + struct Result + { + ObjectOfInterest object_of_interest; + + std::vector deployment; + // kills / total_kills + std::optional> kills; + std::optional costs; + + bool in_detail = false; + bool pause_button = false; + }; + + using ResultOpt = std::optional; + + public: + using VisionHelper::VisionHelper; + virtual ~BattlefieldMatcher() override = default; + + void set_object_of_interest(ObjectOfInterest obj); + void set_total_kills_prompt(int prompt); + + ResultOpt analyze() const; + + protected: + bool hp_flag_analyze() const; + bool kills_flag_analyze() const; + bool pause_button_analyze() const; + + std::vector deployment_analyze() const; // 识别干员 + battle::Role oper_role_analyze(const Rect& roi) const; + bool oper_cooling_analyze(const Rect& roi) const; + int oper_cost_analyze(const Rect& roi) const; + bool oper_available_analyze(const Rect& roi) const; + + std::optional> kills_analyze() const; // 识别击杀数 + std::optional costs_analyze() const; // 识别费用 + bool in_detail_analyze() const; // 识别是否在详情页 + + ObjectOfInterest m_object_of_interest; // 待识别的目标 + int m_total_kills_prompt = 0; // 之前的击杀总数,因为击杀数经常识别不准所以依赖外部传入作为参考 + }; +} // namespace asst