feat: 自动编队自动借助战

This commit is contained in:
晓丶梦丶仁
2024-10-10 19:19:06 +08:00
parent bdf0a6fd27
commit 22ffa6a4fa
11 changed files with 518 additions and 10 deletions

View File

@@ -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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -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<Role, std::string> 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<Role, std::string> RoleToName = {
{ Role::Warrior, "近卫" }, { Role::Pioneer, "先锋" }, { Role::Medic, "医疗" },
{ Role::Tank, "重装" }, { Role::Sniper, "狙击" }, { Role::Caster, "术师" },

View File

@@ -114,6 +114,7 @@
<ClInclude Include="Utils\Algorithm.hpp" />
<ClInclude Include="Utils\File.hpp" />
<ClInclude Include="Utils\LibraryHolder.hpp" />
<ClInclude Include="Vision\Battle\BattleRecruitSupportAnalyzer.h" />
<ClInclude Include="Vision\Roguelike\RoguelikeParameterAnalyzer.h" />
<ClInclude Include="Vision\VisionHelper.h" />
<ClInclude Include="Vision\Battle\BattleFormationAnalyzer.h" />
@@ -302,6 +303,7 @@
<ClCompile Include="Task\SSS\SSSBattleProcessTask.cpp" />
<ClCompile Include="Task\SSS\SSSDropRewardsTaskPlugin.cpp" />
<ClCompile Include="Task\SSS\SSSStageManagerTask.cpp" />
<ClCompile Include="Vision\Battle\BattleRecruitSupportAnalyzer.cpp" />
<ClCompile Include="Vision\Roguelike\RoguelikeParameterAnalyzer.cpp" />
<ClCompile Include="Vision\VisionHelper.cpp" />
<ClCompile Include="Vision\Battle\BattleFormationAnalyzer.cpp" />

View File

@@ -731,6 +731,9 @@
<ClInclude Include="LDExtras.h">
<Filter>Source\Controller</Filter>
</ClInclude>
<ClInclude Include="Vision\Battle\BattleRecruitSupportAnalyzer.h">
<Filter>Source\Vision\Battle</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Vision\VisionHelper.cpp">
@@ -1227,5 +1230,8 @@
<ClCompile Include="LDExtras.cpp">
<Filter>Source\Controller</Filter>
</ClCompile>
<ClCompile Include="Vision\Battle\BattleRecruitSupportAnalyzer.cpp">
<Filter>Source\Vision\Battle</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -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<OperGroup> oper_group,
std::vector<OperGroup>& 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<battle::roguelike::RecruitSupportCharInfo> 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<OperGroup> oper_group, std::vector<OperGroup>& missing)
{
LogTraceFunction;

View File

@@ -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<std::pair<std::string, int>> user_additional);
// 是否追加低信赖干员
void set_add_trust(bool add_trust);
// 设置对指定编队自动编队
void set_select_formation(int index);
std::shared_ptr<std::unordered_map<std::string, std::string>> get_opers_in_formation() const;
enum class DataResource
{
Copilot,
SSSCopilot,
};
void set_data_resource(DataResource resource);
protected:
using OperGroup = std::pair<std::string, std::vector<asst::battle::OperUsage>>;
virtual bool _run() override;
bool add_formation(battle::Role role, std::vector<OperGroup> oper_group, std::vector<OperGroup>& missing);
// 追加附加干员(按部署费用等小分类)
bool add_additional();
// 补充刷信赖的干员,从最小的开始
bool add_trust_operators();
bool enter_selection_page();
bool select_opers_in_cur_page(std::vector<OperGroup>& 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<OperGroup>& groups);
bool select_support_operator(const std::string name, int skill = 0);
std::vector<asst::TemplDetOCRer::Result> analyzer_opers();
std::string m_stage_name;
std::unordered_map<battle::Role, std::vector<OperGroup>> m_formation;
std::vector<OperGroup> m_user_formation;
int m_size_of_operators_in_formation = 0; // 编队中干员个数
std::shared_ptr<std::unordered_map<std::string, std::string>> m_opers_in_formation =
std::make_shared<std::unordered_map<std::string, std::string>>(); // 编队中的干员名称-所属组名
bool m_add_trust = false; // 是否需要追加信赖干员
bool m_add_user_additional = false; // 补用户自定义干员
std::vector<std::pair<std::string, int>> m_user_additional; // 追加干员表,从头往后加
std::string m_support_unit_name;
DataResource m_data_resource = DataResource::Copilot;
std::vector<AdditionalFormation> m_additional;
std::string m_last_oper_name;
int m_select_formation_index = 0;
int m_missing_retry_times = 1; // 识别不到干员的重试次数
};
struct AdditionalFormation

View File

@@ -0,0 +1,223 @@
#include "BattleRecruitSupportAnalyzer.h"
#include <algorithm>
#include <regex>
#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<OcrTaskInfo>("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<OcrTaskInfo>("CharsNameOcrReplace")->replace_map,
Task.get<OcrTaskInfo>("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<std::string, int> 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<MatchTaskInfo>(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<cv::Rect>(roi)); // 截取颜色部分
// 用red通道判断是橙色还是蓝色橙色为好友助战
std::vector<cv::Mat> 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);
}

View File

@@ -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<std::string> 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<battle::roguelike::RecruitSupportCharInfo> m_char_result;
std::vector<std::string> 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);
};
}