diff --git a/docs/3.1-集成文档.md b/docs/3.1-集成文档.md index 0947e0e106..f23145dcc6 100644 --- a/docs/3.1-集成文档.md +++ b/docs/3.1-集成文档.md @@ -200,7 +200,9 @@ AsstTaskId ASSTAPI AsstAppendTask(AsstHandle handle, const char* type, const cha // 投资满了自动停止任务,可选,默认 false "squad": string, // 开局分队,可选,例如 "突击战术分队" 等,默认 "指挥分队" "roles": string, // 开局职业组,可选,例如 "先手必胜" 等,默认 "取长补短" - "core_char": string // 开局干员名,可选,仅支持单个干员中!文!名!。默认识别练度自动选择 + "core_char": string, // 开局干员名,可选,仅支持单个干员中!文!名!。默认识别练度自动选择 + "use_support": bool, // 开局干员是否为助战干员,可选,默认 false + "use_nonfriend_support": bool // 是否可以是非好友助战干员,可选,默认 false,use_support为true时有效, } ``` diff --git a/docs/en-us/3.1-INTEGRATION.md b/docs/en-us/3.1-INTEGRATION.md index 087bf813c1..0340e875d7 100644 --- a/docs/en-us/3.1-INTEGRATION.md +++ b/docs/en-us/3.1-INTEGRATION.md @@ -188,7 +188,9 @@ Appends a task. // Stop the task when investment is full, optional, by default false "squad": string, // Squad name like "assault squad", optional, by default "Default Squad" "roles": string, // Roles, optional - "core_char": string // Operator name, optional. Will recognize auto-selection of levels. + "core_char": string, // Operator name, optional. Will recognize auto-selection of levels. + "use_support": bool, // Whether "core_char" is a support unit, optional, by default false + "use_nonfriend_support": bool // Whether support unit can be non-friend support unit, optional, by default false, valid when use_support=true } ``` diff --git a/docs/ja-jp/3.1-統合ドキュメント.md b/docs/ja-jp/3.1-統合ドキュメント.md index 646a27d27f..b898f9ae33 100644 --- a/docs/ja-jp/3.1-統合ドキュメント.md +++ b/docs/ja-jp/3.1-統合ドキュメント.md @@ -187,7 +187,9 @@ TaskId ASSTAPI AsstAppendTask(AsstHandle handle, const char* type, const char* p // 投資額が一杯になったら動作停止, オプション, デフォルトは false "squad": string, // "突撃戦術分隊" などの分隊指定, オプション, デフォルトは "デフォルト" "roles": string, // 召集組合, オプション - "core_char": string // オペレーター名, オプション. レベルに応じて自動で選択します. + "core_char": string, // オペレーター名, オプション. レベルに応じて自動で選択します. + "use_support": bool, // 開始役員("core_char")がアシストであるかどうかは、オプション、デフォルトは false + "use_nonfriend_support": bool // 非フレンドのアシスタントであるかどうかを、オプション、デフォルトの false、use_supportが true の場合に有効です。 } ``` diff --git a/docs/zh-tw/3.1-集成文件.md b/docs/zh-tw/3.1-集成文件.md index b70c11f54e..945e3775ae 100644 --- a/docs/zh-tw/3.1-集成文件.md +++ b/docs/zh-tw/3.1-集成文件.md @@ -187,7 +187,9 @@ TaskId ASSTAPI AsstAppendTask(AsstHandle handle, const char* type, const char* p // 投資滿了自動停止任務,可選,預設 false "squad": string, // 開局分隊,可選,例如 "突擊戰術分隊" 等,預設 "指揮分隊" "roles": string, // 開局職業組,可選,例如 "先手必勝" 等,預設 "取長補短" - "core_char": string // 開局幹員名,可選,僅支援單個幹員中!文!名! 。預設辨識練度自動選擇 + "core_char": string, // 開局幹員名,可選,僅支援單個幹員中!文!名! 。預設辨識練度自動選擇 + "use_support": bool, // 開局幹員是否為助戰幹員,可選,預設 false + "use_nonfriend_support": bool // 是否可以是非好友助戰幹員,可選,預設 false,use_support為true時有效 } ``` diff --git a/resource/tasks.json b/resource/tasks.json index 16e4499af5..c730086fd2 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -8071,7 +8071,8 @@ "Phantom@Roguelike@ChooseDifficulty": {}, "Phantom@Roguelike@ChooseOper": { "next": [ - "Phantom@Roguelike@ChooseOperConfirm" + "Phantom@Roguelike@ChooseOperConfirm", + "Phantom@Roguelike@RecruitSkip" ] }, "Phantom@Roguelike@CloseCollection": { @@ -8257,7 +8258,8 @@ "next": [ "Mizuki@Roguelike@CloseCollectionContinue", "Mizuki@Roguelike@CloseCollectionClose", - "Mizuki@Roguelike@ChooseOperConfirm" + "Mizuki@Roguelike@ChooseOperConfirm", + "Mizuki@Roguelike@RecruitSkip" ] }, "Mizuki@Roguelike@CloseCollection": { @@ -9135,5 +9137,122 @@ "next": [ "RoguelikeWaitBattleStart" ] + }, + "RoguelikeRecruitSupportOcr": { + "algorithm": "OcrDetect", + "doc": "肉鸽招募助战时干员姓名位置,仅使用roi域", + "text": [], + "roi": [ + 120, + 565, + 1160, + 35 + ] + }, + "RoguelikeRecruitSupportOff1": { + "template": "empty.png", + "rectMove_Doc": "肉鸽招募助战时,干员名下方颜色区域相对名字中心的偏移", + "rectMove": [ + -64, + 54, + 45, + 33 + ], + "specialParams_Doc":"判定是否为好友的r通道阈值", + "specialParams": [ + 70 + ] + }, + "RoguelikeRecruitSupportEliteOff": { + "template": "empty.png", + "rectMove_Doc": "肉鸽招募助战时,精英化状态相对名字中心的偏移", + "rectMove": [ + -26, + -133, + 50, + 41 + ], + "specialParams_Doc":"二值化的阈值", + "specialParams": [ + 250 + ] + }, + "RoguelikeRecruitSupportLevelOff": { + "template": "empty.png", + "rectMove_Doc": "肉鸽招募助战时,等级相对名字中心的偏移", + "rectMove": [ + 18, + -81, + 33, + 21 + ] + }, + "RoguelikeRefreshSupportBtnOcr":{ + "algorithm": "OcrDetect", + "text":["更新助战列表"], + "roi":[ + 1097, + 13, + 164, + 38 + ], + "postDelay": 3000, + "postDelay_Doc": "点击刷新后的等待时间" + }, + "RoguelikeChooseSupportBtnOcr":{ + "algorithm": "OcrDetect", + "text":["选择助战"], + "roi":[ + 1011, + 9, + 177, + 47 + ] + }, + "RoguelikeSupportSwipeRight":{ + "baseTask": "SwipeToTheRight", + "postDelay": 1000 + }, + "RoguelikeSupportSwipeLeft":{ + "baseTask": "SwipeToTheLeft", + "postDelay": 1000 + }, + "Roguelike@RecruitSupportConfirm": { + "algorithm": "OcrDetect", + "action": "ClickSelf", + "text": [ + "招募助战" + ], + "roi": [ + 770, + 500, + 160, + 40 + ], + "preDelay": 200 + }, + "RoguelikeRecruitSupportElite": { + "Doc": "base_task", + "template": "empty.png", + "templThreshold": 0.6 + }, + "RoguelikeRecruitSupportElite0": { + "baseTask": "RoguelikeRecruitSupportElite" + }, + "RoguelikeRecruitSupportElite1": { + "baseTask": "RoguelikeRecruitSupportElite" + }, + "RoguelikeRecruitSupportElite2": { + "baseTask": "RoguelikeRecruitSupportElite" + }, + "RoguelikeRecruitSupportEnterFlag":{ + "template": "Roguelike@RefreshSupportBtn.png", + "action": "DoNothing", + "roi": [ + 1094, + 10, + 172, + 47 + ] } } diff --git a/resource/template/Roguelike@RefreshSupportBtn.png b/resource/template/Roguelike@RefreshSupportBtn.png new file mode 100644 index 0000000000..27a8d9d928 Binary files /dev/null and b/resource/template/Roguelike@RefreshSupportBtn.png differ diff --git a/resource/template/RoguelikeRecruitSupportElite0.png b/resource/template/RoguelikeRecruitSupportElite0.png new file mode 100644 index 0000000000..355106a199 Binary files /dev/null and b/resource/template/RoguelikeRecruitSupportElite0.png differ diff --git a/resource/template/RoguelikeRecruitSupportElite1.png b/resource/template/RoguelikeRecruitSupportElite1.png new file mode 100644 index 0000000000..683fa38715 Binary files /dev/null and b/resource/template/RoguelikeRecruitSupportElite1.png differ diff --git a/resource/template/RoguelikeRecruitSupportElite2.png b/resource/template/RoguelikeRecruitSupportElite2.png new file mode 100644 index 0000000000..44873180b4 Binary files /dev/null and b/resource/template/RoguelikeRecruitSupportElite2.png differ diff --git a/src/MaaCore/Common/AsstBattleDef.h b/src/MaaCore/Common/AsstBattleDef.h index 613a5f3877..6236777431 100644 --- a/src/MaaCore/Common/AsstBattleDef.h +++ b/src/MaaCore/Common/AsstBattleDef.h @@ -221,6 +221,28 @@ namespace asst int level = 0; }; + enum class RoguelikeSupportAnalyzeMode + { + ChooseSupportBtn, + AnalyzeChars, + RefreshSupportBtn + }; + + struct RoguelikeRecruitSupportCharInfo + { + BattleRecruitOperInfo oper_info; + bool is_friend = false; // 是否为好友助战 + int max_elite = 0; // 两次招募后的实际精英化与等级 + int max_level = 0; + }; + + struct RoguelikeRefreshSupportInfo + { + Rect rect; + bool in_cooldown = false; + int remain_secs = 0; // 刷新冷却时间 + }; + using BattleAttackRange = std::vector; static constexpr std::string_view RoguelikePhantomThemeName = "Phantom"; diff --git a/src/MaaCore/Config/Miscellaneous/BattleDataConfig.cpp b/src/MaaCore/Config/Miscellaneous/BattleDataConfig.cpp index b5c8d29ad4..c85754c96f 100644 --- a/src/MaaCore/Config/Miscellaneous/BattleDataConfig.cpp +++ b/src/MaaCore/Config/Miscellaneous/BattleDataConfig.cpp @@ -43,6 +43,9 @@ bool asst::BattleDataConfig::parse(const json::value& json) data.location_type = iter->second; } + const auto& rarity = char_data_json.at("rarity").as_integer(); + data.rarity = rarity; + m_chars.emplace(std::move(name), std::move(data)); } for (const auto& [id, points_json] : json.at("ranges").as_object()) { diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index b73052e7f1..528535353c 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -47,6 +47,7 @@ + @@ -162,6 +163,7 @@ + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index 40f5d8e07a..fa782e535e 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -153,6 +153,9 @@ 源文件\Vision\Roguelike + + 源文件\Vision\Roguelike + 源文件\Config @@ -491,6 +494,9 @@ 源文件\Vision\Roguelike + + 源文件\Vision\Roguelike + 源文件\Config diff --git a/src/MaaCore/Status.h b/src/MaaCore/Status.h index 4fcca06849..e26ccfb171 100644 --- a/src/MaaCore/Status.h +++ b/src/MaaCore/Status.h @@ -42,6 +42,8 @@ namespace asst static inline const std::string RoguelikeCharRarityPrefix = "RoguelikeRarity-"; static inline const std::string RoguelikeCharOverview = "RoguelikeOverview"; static inline const std::string RoguelikeCoreChar = "RoguelikeCoreChar"; + static inline const std::string RoguelikeUseSupport = "RoguelikeUseSupport"; + static inline const std::string RoguelikeUseNonfriendSupport = "RoguelikeUseNonfriendSupport"; static inline const std::string RoguelikeTraderNoLongerBuy = "RoguelikeNoLongerBuy"; static inline const std::string RoguelikeSkillUsagePrefix = "RoguelikeSkillUsage-"; static inline const std::string RoguelikeTeamFullWithoutRookie = "RoguelikeTeamFullWithoutRookie"; diff --git a/src/MaaCore/Task/Interface/RoguelikeTask.cpp b/src/MaaCore/Task/Interface/RoguelikeTask.cpp index b876b359fb..8ac63665f5 100644 --- a/src/MaaCore/Task/Interface/RoguelikeTask.cpp +++ b/src/MaaCore/Task/Interface/RoguelikeTask.cpp @@ -104,6 +104,12 @@ bool asst::RoguelikeTask::set_params(const json::value& params) if (auto core_char_opt = params.find("core_char"); core_char_opt && !core_char_opt->empty()) { m_custom_start_task_ptr->set_custom(RoguelikeCustomType::CoreChar, *core_char_opt); } + if (auto use_support_opt = params.find("use_support"); use_support_opt) { + m_custom_start_task_ptr->set_custom(RoguelikeCustomType::UseSupport, *use_support_opt ? "1" : "0"); + } + if (auto use_nonfriend_opt = params.find("use_nonfriend_support"); use_nonfriend_opt) { + m_custom_start_task_ptr->set_custom(RoguelikeCustomType::UseNonfriendSupport, *use_nonfriend_opt ? "1" : "0"); + } return true; } diff --git a/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.cpp index 4d0d1c5d59..1c6b5f1048 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.cpp @@ -141,6 +141,8 @@ bool asst::RoguelikeCustomStartTaskPlugin::hijack_core_char() sleep(Task.get("RoguelikeCustom-HijackCoChar")->pre_delay); + status()->set_str(Status::RoguelikeUseSupport, m_customs[RoguelikeCustomType::UseSupport]); + status()->set_str(Status::RoguelikeUseNonfriendSupport, m_customs[RoguelikeCustomType::UseNonfriendSupport]); status()->set_str(Status::RoguelikeCoreChar, char_name); return true; } diff --git a/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.h b/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.h index 1d79a11e15..069dc59247 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.h +++ b/src/MaaCore/Task/Roguelike/RoguelikeCustomStartTaskPlugin.h @@ -9,6 +9,8 @@ namespace asst Squad, // 分队类型, like 指挥分队, 矛头分队, etc Roles, // 职业类型, like 先手必胜, 稳扎稳打, etc CoreChar, // 首选干员, 干员名 + UseSupport, // 使用助战 + UseNonfriendSupport // 可以使用非好友助战 // CoCoreChar, // 次选干员, 干员名 }; diff --git a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp index 48a8b85c32..81428dc988 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp @@ -1,13 +1,16 @@ #include "RoguelikeRecruitTaskPlugin.h" -#include "Controller.h" -#include "Vision/Roguelike/RoguelikeRecruitImageAnalyzer.h" #include "Config/Miscellaneous/BattleDataConfig.h" #include "Config/Roguelike/RoguelikeRecruitConfig.h" #include "Config/TaskData.h" +#include "Controller.h" #include "Status.h" #include "Task/ProcessTask.h" #include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Vision/OcrImageAnalyzer.h" +#include "Vision/Roguelike/RoguelikeRecruitImageAnalyzer.h" +#include "Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.h" bool asst::RoguelikeRecruitTaskPlugin::verify(AsstMsg msg, const json::value& details) const { @@ -51,8 +54,17 @@ bool asst::RoguelikeRecruitTaskPlugin::_run() { LogTraceFunction; - if (check_core_char()) { - return true; + // 开局干员 + bool use_support = get_status_bool(Status::RoguelikeUseSupport); + if (use_support) { + if (check_support_char()) { + return true; + } + } + else { + if (check_core_char()) { + return true; + } } bool recruited = false; @@ -370,6 +382,93 @@ bool asst::RoguelikeRecruitTaskPlugin::check_char(const std::string& char_name, return false; } +bool asst::RoguelikeRecruitTaskPlugin::check_support_char() +{ + LogTraceFunction; + const int MaxRefreshTimes = 3; + + auto core_opt = status()->get_str(Status::RoguelikeCoreChar); + status()->set_str(Status::RoguelikeCoreChar, ""); + if (core_opt && !core_opt->empty()) { + if (check_support_char(core_opt.value(), MaxRefreshTimes)) return true; + } + return false; +} + +bool asst::RoguelikeRecruitTaskPlugin::check_support_char(const std::string& name, const int max_refresh) +{ + LogTraceFunction; + + // 判断是否存在“选择助战”按钮,存在则点击 + auto screen_choose = ctrler()->get_image(); + RoguelikeRecruitSupportAnalyzer analyzer_choose(screen_choose); + analyzer_choose.set_mode(RoguelikeSupportAnalyzeMode::ChooseSupportBtn); + if (!analyzer_choose.analyze()) { // 非开局招募,无助战按钮 + return false; + } + const auto& choose_btn_rect = analyzer_choose.get_result_choose_support(); + Log.info(__FUNCTION__, "| check choose support btn ", choose_btn_rect); + ctrler()->click(choose_btn_rect); + ProcessTask(*this, { "RoguelikeRecruitSupportEnterFlag" }).run(); // 等待页面加载 + + + // 识别所有干员,应该最多两页 + const int SwipeTimes = 1; + + std::vector satisfied_chars; + for (int retry = 0; retry <= max_refresh; ++retry) { + for (int swipe = 0; swipe < SwipeTimes; ++swipe) { + auto screen_char = ctrler()->get_image(); + RoguelikeRecruitSupportAnalyzer analyzer_char(screen_char); + analyzer_char.set_mode(RoguelikeSupportAnalyzeMode::AnalyzeChars); + analyzer_char.set_required({ name }); + if (analyzer_char.analyze()) { + auto& chars_page = analyzer_char.get_result_char(); + + bool use_nonfriend_support = get_status_bool(Status::RoguelikeUseNonfriendSupport); + auto check_satisfiy = [&use_nonfriend_support](const RoguelikeRecruitSupportCharInfo& 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_satisfiy); + + if (satisfied_chars.size()) break; + } + ProcessTask(*this, { "RoguelikeSupportSwipeRight" }).run(); + } + if (satisfied_chars.size()) break; + + // 刷新助战 + if (retry >= max_refresh) break; + auto screen_refresh = ctrler()->get_image(); + RoguelikeRecruitSupportAnalyzer analyzer_refresh(screen_refresh); + analyzer_refresh.set_mode(RoguelikeSupportAnalyzeMode::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(); + select_oper(satisfied_char.oper_info); + + // 确认选择 + ProcessTask(*this, { "Roguelike@RecruitSupportConfirm" }).run(); + return true; +} + bool asst::RoguelikeRecruitTaskPlugin::check_core_char() { LogTraceFunction; @@ -400,6 +499,12 @@ void asst::RoguelikeRecruitTaskPlugin::select_oper(const BattleRecruitOperInfo& status()->set_str(Status::RoguelikeCharOverview, overview.to_string()); } +inline bool asst::RoguelikeRecruitTaskPlugin::get_status_bool(const std::string& key) +{ + auto value_opt = status()->get_str(key); + return value_opt && value_opt->size() == 1 && value_opt.value()[0] == '1'; +} + void asst::RoguelikeRecruitTaskPlugin::swipe_to_the_left_of_operlist(int loop_times) { for (int i = 0; i != loop_times; ++i) { diff --git a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.h b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.h index 3466badb98..d366343b3b 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.h +++ b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.h @@ -29,6 +29,9 @@ namespace asst void slowly_swipe(bool to_left); private: + bool check_support_char(); + bool check_support_char(const std::string& name, const int max_refresh); + bool check_core_char(); BattleRole get_oper_role(const std::string& name); bool is_oper_melee(const std::string& name); @@ -40,5 +43,7 @@ namespace asst // 返回值: 招募结果 (true: 成功; false: 失败) bool check_char(const std::string& char_name, bool is_rtl = false); void select_oper(const BattleRecruitOperInfo& oper); + + inline bool get_status_bool(const std::string& key); }; } diff --git a/src/MaaCore/Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.cpp b/src/MaaCore/Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.cpp new file mode 100644 index 0000000000..a6ec0e5370 --- /dev/null +++ b/src/MaaCore/Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.cpp @@ -0,0 +1,193 @@ +#include "RoguelikeRecruitSupportAnalyzer.h" + +#include +#include + +#include "Config/Miscellaneous/BattleDataConfig.h" +#include "Config/TaskData.h" +#include "Controller.h" +#include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Vision/MatchImageAnalyzer.h" +#include "Vision/OcrImageAnalyzer.h" +#include "Vision/OcrWithFlagTemplImageAnalyzer.h" + +bool asst::RoguelikeRecruitSupportAnalyzer::analyze() +{ + LogTraceFunction; + + if (m_mode == RoguelikeSupportAnalyzeMode::ChooseSupportBtn) { + // 识别“选择助战” + OcrImageAnalyzer 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 == RoguelikeSupportAnalyzeMode::AnalyzeChars) { + // 识别干员 + OcrImageAnalyzer analyzer(m_image); + analyzer.set_roi(Task.get("RoguelikeRecruitSupportOcr")->roi); + analyzer.set_required(m_required); + 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()); + + RoguelikeRecruitSupportCharInfo 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_elite, char_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 == RoguelikeSupportAnalyzeMode::RefreshSupportBtn) { + // 识别“更新助战列表” + OcrImageAnalyzer analyzer(m_image); + const auto& task = Task.get("RoguelikeRefreshSupportBtnOcr"); + analyzer.set_roi(task->roi); + if (!analyzer.analyze()) return false; + const auto& results = analyzer.get_result(); + for (const auto& result : results) { + Log.info(__FUNCTION__, "| RefreshSupportBtn parse `", result.text, "`", result.score); + if (std::count(task->text.begin(), task->text.end(), result.text)) { + m_refresh_result = { result.rect, false, 0 }; + return true; + } + else if (std::regex_match(result.text, std::regex("[0-9]{2}:[0-9]{2}:[0-9]{2}"))) { + const auto& hour = std::atoi(result.text.substr(2).c_str()); + const auto& min = std::atoi(result.text.substr(3, 2).c_str()); + const auto& sec = std::atoi(result.text.substr(7, 2).c_str()); + m_refresh_result = { result.rect, true, 3600 * hour + 60 * min + sec }; + return true; + } + } + return false; + } + + return false; +} + +int asst::RoguelikeRecruitSupportAnalyzer::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) { + MatchImageAnalyzer 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_threshold); + + 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::RoguelikeRecruitSupportAnalyzer::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::RoguelikeRecruitSupportAnalyzer::match_level(const Rect& roi) +{ + LogTraceFunction; + + OcrWithPreprocessImageAnalyzer analyzer(m_image); + analyzer.set_task_info("NumberOcrReplace"); + analyzer.set_roi(roi); + analyzer.set_expansion(1); + + if (!analyzer.analyze()) { + return -1; + } + + Log.info(__FUNCTION__, "| ", roi, "`", analyzer.get_result().front().text, "`"); + const std::string& level = analyzer.get_result().front().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/Roguelike/RoguelikeRecruitSupportAnalyzer.h b/src/MaaCore/Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.h new file mode 100644 index 0000000000..bb2e319716 --- /dev/null +++ b/src/MaaCore/Vision/Roguelike/RoguelikeRecruitSupportAnalyzer.h @@ -0,0 +1,34 @@ +#pragma once +#include "Vision/AbstractImageAnalyzer.h" + +#include "Common/AsstBattleDef.h" + +namespace asst +{ + class RoguelikeRecruitSupportAnalyzer final : public AbstractImageAnalyzer + { + public: + using AbstractImageAnalyzer::AbstractImageAnalyzer; + virtual ~RoguelikeRecruitSupportAnalyzer() noexcept override = default; + + bool analyze() override; + + void set_mode(RoguelikeSupportAnalyzeMode 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: + RoguelikeSupportAnalyzeMode m_mode; + Rect m_choose_support_result; + RoguelikeRefreshSupportInfo 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); + }; +}