diff --git a/resource/tasks.json b/resource/tasks.json index 530457bb9f..ba77de5db0 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -3503,6 +3503,37 @@ ["^资深.+", "资深干员"] ] }, + "SelectSupportEnter": { + "algorithm": "JustReturn", + "next": ["SelectSupportStartSelect", "SelectSupportBackToFormationPage"] + }, + "SelectSupportBackToFormationPage": { + "action": "ClickSelf", + "roi": [0, 0, 144, 142], + "next": ["SelectSupportEnter"] + }, + "SelectSupportStartSelect": { + "action": "ClickSelf", + "roi": [982, 228, 126, 176] + }, + "SelectSupportOperSkill0": { + "algorithm": "JustReturn" + }, + "SelectSupportOperSkill1": { + "template": "SelectSupportOperSkill.png", + "action": "ClickSelf", + "roi": [781, 320, 66, 38] + }, + "SelectSupportOperSkill2": { + "template": "SelectSupportOperSkill.png", + "action": "ClickSelf", + "roi": [900, 320, 64, 38] + }, + "SelectSupportOperSkill3": { + "template": "SelectSupportOperSkill.png", + "action": "ClickSelf", + "roi": [1010, 320, 70, 38] + }, "RecruitRefresh": { "action": "ClickSelf", "roi": [850, 340, 220, 170], @@ -4939,6 +4970,43 @@ "BattleOperRoleTank": {}, "BattleOperRoleWarrior": {}, "BattleOperRoleDrone": {}, + "SupportOperRoleBase": { + "algorithm": "JustReturn", + "action": "ClickRect", + "postDelay": 1000 + }, + "SupportOperRoleCaster": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 406, 55, 50] + }, + "SupportOperRoleMedic": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 483, 55, 50] + }, + "SupportOperRolePioneer": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 102, 55, 50] + }, + "SupportOperRoleSniper": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 330, 55, 50] + }, + "SupportOperRoleSpecial": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 637, 55, 50] + }, + "SupportOperRoleSupport": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 556, 55, 50] + }, + "SupportOperRoleTank": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 252, 55, 50] + }, + "SupportOperRoleWarrior": { + "baseTask": "SupportOperRoleBase", + "specificRect": [30, 174, 55, 50] + }, "BattleOperCost": { "template": "empty.png", "rectMove": [-10, 12, 30, 17] diff --git a/resource/template/SelectSupportBackToFormationPage.png b/resource/template/SelectSupportBackToFormationPage.png new file mode 100644 index 0000000000..9472c64fd5 Binary files /dev/null and b/resource/template/SelectSupportBackToFormationPage.png differ diff --git a/resource/template/SelectSupportOperSkill.png b/resource/template/SelectSupportOperSkill.png new file mode 100644 index 0000000000..b0feb8a3a7 Binary files /dev/null and b/resource/template/SelectSupportOperSkill.png differ diff --git a/resource/template/SelectSupportStartSelect.png b/resource/template/SelectSupportStartSelect.png new file mode 100644 index 0000000000..f3245e5d15 Binary files /dev/null and b/resource/template/SelectSupportStartSelect.png differ diff --git a/src/MaaCore/Common/AsstBattleDef.h b/src/MaaCore/Common/AsstBattleDef.h index 089968d324..5935af7d6c 100644 --- a/src/MaaCore/Common/AsstBattleDef.h +++ b/src/MaaCore/Common/AsstBattleDef.h @@ -341,9 +341,20 @@ struct RefreshSupportInfo namespace asst { -inline std::string enum_to_string(asst::battle::Role role) +inline std::string enum_to_string(asst::battle::Role role, bool en = false) { using asst::battle::Role; + if (en) { + static const std::unordered_map RoleToName_EN = { + { Role::Warrior, "Warrior" }, { Role::Pioneer, "Pioneer" }, { Role::Medic, "Medic" }, + { Role::Tank, "Tank" }, { Role::Sniper, "Sniper" }, { Role::Caster, "Caster" }, + { Role::Support, "Support" }, { Role::Special, "Special" }, { Role::Drone, "Drone" }, + }; + if (auto iter = RoleToName_EN.find(role); iter != RoleToName_EN.end()) { + return iter->second; + } + return "Unknown"; + } static const std::unordered_map RoleToName = { { Role::Warrior, "近卫" }, { Role::Pioneer, "先锋" }, { Role::Medic, "医疗" }, { Role::Tank, "重装" }, { Role::Sniper, "狙击" }, { Role::Caster, "术师" }, diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 95c3f965bc..f61dde3b3d 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -114,6 +114,7 @@ + @@ -302,6 +303,7 @@ + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index a83810b404..d04d3a3431 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -731,6 +731,9 @@ Source\Controller + + Source\Vision\Battle + @@ -1227,5 +1230,8 @@ Source\Controller + + Source\Vision\Battle + \ No newline at end of file diff --git a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp index cc99220a53..97bc0ba103 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp @@ -11,6 +11,7 @@ #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" #include "Vision/MultiMatcher.h" +#include "Vision/Battle/BattleRecruitSupportAnalyzer.h" void asst::BattleFormationTask::append_additional_formation(AdditionalFormation formation) { @@ -69,10 +70,15 @@ bool asst::BattleFormationTask::_run() } if (!missing_operators.empty()) { + bool support = false; if (missing_operators.size() == 1) { - // TODO: 自动借助战? + // 记得加上 SupportUnitUsage 的壳 + support = select_support_operator( + missing_operators.front().first, + missing_operators.front().second.front().skill); } - + if (support) return true; + report_missing_operators(missing_operators); return false; @@ -115,10 +121,94 @@ bool asst::BattleFormationTask::_run() return true; } -bool asst::BattleFormationTask::add_formation( - battle::Role role, - std::vector oper_group, - std::vector& missing) +bool asst::BattleFormationTask::select_support_operator(const std::string name, int skill) +{ + ProcessTask(*this, { "SelectSupportEnter" }).run(); + ProcessTask(*this, { "SupportOperRole" + enum_to_string(BattleData.get_role(name), true) }).run(); + + // 识别所有干员,应该最多两页 + const int MaxPageCnt = 2; + const int max_refresh = 10; + + std::vector satisfied_chars; + for (int retry = 0; retry <= max_refresh; ++retry) { + if (need_exit()) return false; + for (int page = 0; page < MaxPageCnt; ++page) { + auto screen_char = ctrler()->get_image(); + BattleRecruitSupportAnalyzer analyzer_char(screen_char); + analyzer_char.set_mode(battle::roguelike::SupportAnalyzeMode::AnalyzeChars); + analyzer_char.set_required({ name }); + if (analyzer_char.analyze()) { + auto& chars_page = analyzer_char.get_result_char(); + + bool use_nonfriend_support = true; + auto check_satisfy = [&use_nonfriend_support](const battle::roguelike::RecruitSupportCharInfo& chara) { + return chara.is_friend || use_nonfriend_support; + }; + std::copy_if( + chars_page.begin(), + chars_page.end(), + std::inserter(satisfied_chars, std::begin(satisfied_chars)), + check_satisfy); + + if (satisfied_chars.size()) { + break; + } + } + if (page != MaxPageCnt - 1) { + ProcessTask(*this, { "RoguelikeSupportSwipeRight" }).run(); + } + } + if (satisfied_chars.size()) { + break; + } + + // 刷新助战 + if (retry >= max_refresh) { + break; + } + auto screen_refresh = ctrler()->get_image(); + BattleRecruitSupportAnalyzer analyzer_refresh(screen_refresh); + analyzer_refresh.set_mode(battle::roguelike::SupportAnalyzeMode::RefreshSupportBtn); + if (!analyzer_refresh.analyze()) { + click_return_button(); + return false; + } + auto& refresh_info = analyzer_refresh.get_result_refresh(); + if (refresh_info.in_cooldown) { + sleep(refresh_info.remain_secs * 1000); + } + ctrler()->click(refresh_info.rect); + sleep(Task.get("RoguelikeRefreshSupportBtnOcr")->post_delay); + ProcessTask(*this, { "RoguelikeSupportSwipeLeft" }).run(); + } + if (satisfied_chars.empty()) { + // 找不到需要的助战干员,返回正常招募逻辑 + Log.info(__FUNCTION__, "| can't find support char `", name, "`"); + click_return_button(); + return false; + } + + // 点击干员并记录信息 + auto& satisfied_char = satisfied_chars.front(); + + // 选择干员 + const battle::roguelike::Recruitment& oper = satisfied_char.oper_info; + + Log.info(__FUNCTION__, "| Choose support oper:", oper.name, "( elite", oper.elite, "level", oper.level, ")"); + + ctrler()->click(oper.rect); + + // 选择技能 + ProcessTask(*this, { "SelectSupportOperSkill" + std::to_string(skill) }).run(); + + // 确认选择 + ProcessTask(*this, { "RecruitSupportConfirm" }).run(); + + return false; +} + +bool asst::BattleFormationTask::add_formation(battle::Role role, std::vector oper_group, std::vector& missing) { LogTraceFunction; diff --git a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h index d62ad0b87d..799504e6f1 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h +++ b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h @@ -15,9 +15,80 @@ public: enum class Filter { - None, - Trust, - Cost, + public: + using AbstractTask::AbstractTask; + virtual ~BattleFormationTask() override = default; + + enum class Filter + { + None, + Trust, + Cost, + }; + struct AdditionalFormation + { + Filter filter = Filter::None; + bool double_click_filter = true; + battle::RoleCounts role_counts; + }; + void append_additional_formation(AdditionalFormation formation); + + void set_support_unit_name(std::string name); + // 是否追加自定干员 + void set_add_user_additional(bool add_user_additional); + // 设置追加自定干员列表 + void set_user_additional(std::vector> user_additional); + // 是否追加低信赖干员 + void set_add_trust(bool add_trust); + // 设置对指定编队自动编队 + void set_select_formation(int index); + std::shared_ptr> get_opers_in_formation() const; + + enum class DataResource + { + Copilot, + SSSCopilot, + }; + void set_data_resource(DataResource resource); + + protected: + using OperGroup = std::pair>; + + virtual bool _run() override; + bool add_formation(battle::Role role, std::vector oper_group, std::vector& missing); + // 追加附加干员(按部署费用等小分类) + bool add_additional(); + // 补充刷信赖的干员,从最小的开始 + bool add_trust_operators(); + bool enter_selection_page(); + bool select_opers_in_cur_page(std::vector& groups); + void swipe_page(); + void swipe_to_the_left(int times = 2); + bool confirm_selection(); + bool click_role_table(battle::Role role); + bool parse_formation(); + bool select_formation(int select_index); + bool select_random_support_unit(); + void report_missing_operators(std::vector& groups); + bool select_support_operator(const std::string name, int skill = 0); + + std::vector analyzer_opers(); + + std::string m_stage_name; + std::unordered_map> m_formation; + std::vector m_user_formation; + int m_size_of_operators_in_formation = 0; // 编队中干员个数 + std::shared_ptr> m_opers_in_formation = + std::make_shared>(); // 编队中的干员名称-所属组名 + bool m_add_trust = false; // 是否需要追加信赖干员 + bool m_add_user_additional = false; // 补用户自定义干员 + std::vector> m_user_additional; // 追加干员表,从头往后加 + std::string m_support_unit_name; + DataResource m_data_resource = DataResource::Copilot; + std::vector m_additional; + std::string m_last_oper_name; + int m_select_formation_index = 0; + int m_missing_retry_times = 1; // 识别不到干员的重试次数 }; struct AdditionalFormation diff --git a/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.cpp b/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.cpp new file mode 100644 index 0000000000..c404326f7b --- /dev/null +++ b/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.cpp @@ -0,0 +1,223 @@ +#include "BattleRecruitSupportAnalyzer.h" + +#include +#include + +#include "Config/Miscellaneous/BattleDataConfig.h" +#include "Config/TaskData.h" +#include "Controller/Controller.h" +#include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Vision/Matcher.h" +#include "Vision/OCRer.h" +#include "Vision/RegionOCRer.h" +#include "Vision/TemplDetOCRer.h" + +bool asst::BattleRecruitSupportAnalyzer::analyze() +{ + LogTraceFunction; + + if (m_mode == battle::roguelike::SupportAnalyzeMode::ChooseSupportBtn) { + // 识别“选择助战” + OCRer analyzer(m_image); + const auto& task = Task.get("RoguelikeChooseSupportBtnOcr"); + analyzer.set_roi(task->roi); + analyzer.set_required(task->text); + if (!analyzer.analyze()) { + return false; + } + m_choose_support_result = analyzer.get_result().front().rect; + Log.info(__FUNCTION__, "| ChooseSupportBtn"); + return true; + } + else if (m_mode == battle::roguelike::SupportAnalyzeMode::AnalyzeChars) { + // 识别干员 + OCRer analyzer(m_image); + analyzer.set_roi(Task.get("RoguelikeRecruitSupportOcr")->roi); + analyzer.set_required(m_required); + analyzer.set_replace( + Task.get("CharsNameOcrReplace")->replace_map, + Task.get("CharsNameOcrReplace")->replace_full); + if (!analyzer.analyze()) { + return false; + } + + const auto& char_name_rects = analyzer.get_result(); + const auto& task_off1 = Task.get("RoguelikeRecruitSupportOff1"); + const auto& task_off_elite = Task.get("RoguelikeRecruitSupportEliteOff"); + const auto& task_off_level = Task.get("RoguelikeRecruitSupportLevelOff"); + m_char_result.clear(); + for (const auto& char_rect : char_name_rects) { + Rect name_rect = char_rect.rect; + name_rect.x = name_rect.x + name_rect.width / 2; + + // 识别等级区域 + Rect level_roi = name_rect.move(task_off_level->rect_move); + int char_level = match_level(level_roi); + if (char_level <= 0) { + // 等级识别失败,可能希望不足,舍弃结果 + Log.info(__FUNCTION__, "| match_level failed ", char_rect.text, char_level); + continue; + } + + // 判断是否为好友助战 + Rect color_roi = name_rect.move(task_off1->rect_move); + bool is_friend = judge_is_friend(color_roi, task_off1->special_params.front()); + + // 匹配精英化状态 + Rect elite_roi = name_rect.move(task_off_elite->rect_move); + int char_elite = match_elite(elite_roi, task_off_elite->special_params.front()); + + battle::roguelike::RecruitSupportCharInfo char_info { + { char_rect.text, char_rect.rect, char_elite, char_level }, + is_friend, + char_elite, + char_level + }; + + // 助战招募最多精一 + int rarity = BattleData.get_rarity(char_rect.text); + if (rarity == 0) { + // 非干员名,可能是屏幕边角,舍弃结果 + Log.info(__FUNCTION__, "| can't get rarity of `", char_rect.text, "`"); + continue; + } + else if (rarity <= 3) { + // 无需处理 + } + else { + if (char_elite == 2) { + char_info.oper_info.elite = 1; + char_info.oper_info.level = 60 + 10 * (rarity - 4); + } + } + + Log.info( + __FUNCTION__, + "| AnalyzeChars append ", + char_info.oper_info.name, + char_info.oper_info.rect, + char_info.oper_info.elite, + char_info.oper_info.level, + is_friend, + char_info.max_elite, + char_info.max_level); + m_char_result.push_back(char_info); + } + return !m_char_result.empty(); + } + else if (m_mode == battle::roguelike::SupportAnalyzeMode::RefreshSupportBtn) { + // 识别“更新助战列表” + OCRer analyzer(m_image); + + // 未处在冷却时间 + analyzer.set_task_info("RoguelikeRefreshSupportBtnOcr"); + if (analyzer.analyze()) { + m_refresh_result = { analyzer.get_result().front().rect, false, 0 }; + return true; + } + + // 刷新冷却中 + analyzer.set_required({}); + analyzer.set_replace({ { ":", ":" } }); + if (!analyzer.analyze()) { + Log.info(__FUNCTION__, "| RefreshSupportBtn analyse failed"); + return false; + } + const auto& results = analyzer.get_result(); + for (const auto& result : results) { + Log.info(__FUNCTION__, "| RefreshSupportBtn parse `", result.text, "`", result.score); + std::smatch match_results; + if (std::regex_search(result.text, match_results, std::regex("[0-9]{2}:[0-9]{2}:[0-9]{2}"))) { + const auto& match_str = match_results[0].str(); + const auto& hour = std::atoi(match_str.substr(2).c_str()); + const auto& min = std::atoi(match_str.substr(3, 2).c_str()); + const auto& sec = std::atoi(match_str.substr(7, 2).c_str()); + m_refresh_result = { result.rect, true, 3600 * hour + 60 * min + sec }; + return true; + } + } + Log.info(__FUNCTION__, "| RefreshSupportBtn failed: no matched reusults"); + return false; + } + + return false; +} + +int asst::BattleRecruitSupportAnalyzer::match_elite(const Rect& roi, const int threshold) +{ + LogTraceFunction; + + static const std::unordered_map EliteTaskName = { + { "RoguelikeRecruitSupportElite0", 0 }, + { "RoguelikeRecruitSupportElite1", 1 }, + { "RoguelikeRecruitSupportElite2", 2 }, + }; + + int elite_result = 0; + double max_score = 0; + + // 二值化,避免干员立绘影响识别,精英化标识主体颜色是纯白色 + cv::Mat bin_img; + cv::threshold(m_image, bin_img, threshold, 255, cv::THRESH_BINARY); + + for (const auto& [task_name, elite] : EliteTaskName) { + Matcher analyzer(bin_img); + auto task_ptr = Task.get(task_name); + analyzer.set_task_info(task_ptr); + analyzer.set_roi(roi); + analyzer.set_threshold(Task.get(task_name)->templ_thresholds.front()); + + if (!analyzer.analyze()) { + continue; + } + + double score = analyzer.get_result().score; + if (score > max_score) { + max_score = score; + elite_result = elite; + } + } + + Log.info(__FUNCTION__, "| ", roi, elite_result, max_score); + return elite_result; +} + +int asst::BattleRecruitSupportAnalyzer::judge_is_friend(const Rect& roi, const double threshold) +{ + LogTraceFunction; + + const auto& color_img = m_image(make_rect(roi)); // 截取颜色部分 + + // 用red通道判断是橙色还是蓝色,橙色为好友助战 + std::vector channels; + split(color_img, channels); + const auto& red_channel = channels.at(2); + auto r_mean = cv::mean(red_channel)[0]; + bool is_friend = (r_mean > threshold); + + Log.info(__FUNCTION__, "| ", roi, r_mean, threshold); + + return is_friend; +} + +int asst::BattleRecruitSupportAnalyzer::match_level(const Rect& roi) +{ + LogTraceFunction; + + RegionOCRer analyzer(m_image); + analyzer.set_task_info("NumberOcrReplace"); + analyzer.set_roi(roi); + analyzer.set_bin_expansion(1); + + if (!analyzer.analyze()) { + return -1; + } + + Log.info(__FUNCTION__, "| ", roi, "`", analyzer.get_result().text, "`"); + const std::string& level = analyzer.get_result().text; + if (level.empty() || !ranges::all_of(level, [](char c) -> bool { return std::isdigit(c); })) { + return 0; + } + return std::stoi(level); +} diff --git a/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.h b/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.h new file mode 100644 index 0000000000..c9e37624cf --- /dev/null +++ b/src/MaaCore/Vision/Battle/BattleRecruitSupportAnalyzer.h @@ -0,0 +1,37 @@ +#pragma once +#include "Vision/VisionHelper.h" + +#include "Common/AsstBattleDef.h" + +namespace asst +{ +class BattleRecruitSupportAnalyzer final : public VisionHelper +{ +public: + using VisionHelper::VisionHelper; + virtual ~BattleRecruitSupportAnalyzer() noexcept override = default; + + bool analyze(); + + void set_mode(battle::roguelike::SupportAnalyzeMode mode) noexcept { m_mode = mode; } + + void set_required(std::vector required) noexcept { m_required = required; } + + const auto& get_result_choose_support() { return m_choose_support_result; } + + const auto& get_result_char() { return m_char_result; } + + const auto& get_result_refresh() { return m_refresh_result; } + +private: + battle::roguelike::SupportAnalyzeMode m_mode = battle::roguelike::SupportAnalyzeMode::ChooseSupportBtn; + Rect m_choose_support_result; + battle::roguelike::RefreshSupportInfo m_refresh_result; + std::vector m_char_result; + std::vector m_required; + + int judge_is_friend(const Rect& roi, const double r_threshold); + int match_elite(const Rect& roi, const int threshold); + int match_level(const Rect& roi); +}; +}