mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-17 01:59:33 +08:00
1078 lines
38 KiB
C++
1078 lines
38 KiB
C++
#include "RoguelikeBattleTaskPlugin.h"
|
||
|
||
#include "Utils/AsstRanges.hpp"
|
||
#include <chrono>
|
||
#include <future>
|
||
|
||
#include "Utils/NoWarningCV.h"
|
||
|
||
#include "../Sub/ProcessTask.h"
|
||
#include "Controller.h"
|
||
#include "ImageAnalyzer/BattleImageAnalyzer.h"
|
||
#include "ImageAnalyzer/General/MatchImageAnalyzer.h"
|
||
#include "ImageAnalyzer/General/OcrWithPreprocessImageAnalyzer.h"
|
||
#include "Resource/BattleDataConfiger.h"
|
||
#include "Resource/RoguelikeCopilotConfiger.h"
|
||
#include "Resource/TilePack.h"
|
||
#include "RuntimeStatus.h"
|
||
#include "TaskData.h"
|
||
#include "Utils/AsstImageIo.hpp"
|
||
#include "Utils/Logger.hpp"
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::verify(AsstMsg msg, const json::value& details) const
|
||
{
|
||
if (msg != AsstMsg::SubTaskCompleted || details.get("subtask", std::string()) != "ProcessTask") {
|
||
return false;
|
||
}
|
||
|
||
if (details.at("details").at("task").as_string() == "Roguelike1StartAction") {
|
||
return true;
|
||
}
|
||
else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::set_stage_name(std::string stage)
|
||
{
|
||
m_stage_name = std::move(stage);
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::_run()
|
||
{
|
||
using namespace std::chrono_literals;
|
||
|
||
bool gotten_info = get_stage_info();
|
||
if (!gotten_info) {
|
||
return true;
|
||
}
|
||
if (!wait_start()) {
|
||
return false;
|
||
}
|
||
|
||
speed_up();
|
||
|
||
bool timeout = false;
|
||
auto start_time = std::chrono::steady_clock::now();
|
||
while (!need_exit()) {
|
||
// 不在战斗场景,且已使用过了干员,说明已经打完了,就结束循环
|
||
if (!auto_battle() && m_opers_used) {
|
||
break;
|
||
}
|
||
if (std::chrono::steady_clock::now() - start_time > 5min) {
|
||
timeout = true;
|
||
break;
|
||
}
|
||
}
|
||
// 超过时间限制了,一般是某个怪被干员卡住了,一直不结束。
|
||
if (timeout) {
|
||
Log.info("Timeout, retreat!");
|
||
all_melee_retreat();
|
||
|
||
start_time = std::chrono::steady_clock::now();
|
||
while (!need_exit()) {
|
||
if (!auto_battle()) {
|
||
break;
|
||
}
|
||
if (std::chrono::steady_clock::now() - start_time > 1min) {
|
||
Log.info("Timeout again, abandon!");
|
||
abandon();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
clear();
|
||
|
||
return true;
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::wait_for_start()
|
||
{
|
||
ProcessTask(*this, { "Roguelike1WaitBattleStart" }).set_task_delay(0).set_retry_times(0).run();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::get_stage_info()
|
||
{
|
||
LogTraceFunction;
|
||
|
||
wait_for_start();
|
||
|
||
bool calced = false;
|
||
|
||
if (m_stage_name.empty()) {
|
||
const auto stage_name_task_ptr = Task.get("BattleStageName");
|
||
sleep(stage_name_task_ptr->pre_delay);
|
||
|
||
constexpr int StageNameRetryTimes = 50;
|
||
for (int i = 0; i != StageNameRetryTimes; ++i) {
|
||
cv::Mat image = m_ctrler->get_image();
|
||
OcrWithPreprocessImageAnalyzer name_analyzer(image);
|
||
|
||
name_analyzer.set_task_info(stage_name_task_ptr);
|
||
if (!name_analyzer.analyze()) {
|
||
continue;
|
||
}
|
||
|
||
for (const auto& tr : name_analyzer.get_result()) {
|
||
auto side_info = Tile.calc(tr.text, true);
|
||
if (side_info.empty()) {
|
||
continue;
|
||
}
|
||
m_stage_name = tr.text;
|
||
m_side_tile_info = std::move(side_info);
|
||
m_normal_tile_info = Tile.calc(m_stage_name, false);
|
||
calced = true;
|
||
break;
|
||
}
|
||
if (calced) {
|
||
break;
|
||
}
|
||
std::this_thread::yield();
|
||
}
|
||
}
|
||
else {
|
||
m_side_tile_info = Tile.calc(m_stage_name, true);
|
||
m_normal_tile_info = Tile.calc(m_stage_name, false);
|
||
calced = true;
|
||
}
|
||
|
||
auto opt = RoguelikeCopilot.get_stage_data(m_stage_name);
|
||
if (opt && !opt->replacement_home.empty()) {
|
||
m_homes = opt->replacement_home;
|
||
auto homes_pos = m_homes | views::transform(&ReplacementHome::location);
|
||
auto invalid_homes_pos =
|
||
homes_pos | views::filter([&](const auto& home_pos) { return !m_normal_tile_info.contains(home_pos); });
|
||
if (!invalid_homes_pos.empty()) {
|
||
Log.error("No replacement homes point:", invalid_homes_pos);
|
||
}
|
||
Log.info("replacement home:", homes_pos);
|
||
m_blacklist_location = opt->blacklist_location;
|
||
m_stage_use_dice = opt->use_dice_stage;
|
||
m_role_order = opt->role_order;
|
||
}
|
||
else {
|
||
for (const auto& [loc, side] : m_normal_tile_info) {
|
||
if (side.key == TilePack::TileKey::Home) {
|
||
m_homes.emplace_back(ReplacementHome { loc, BattleDeployDirection::None });
|
||
}
|
||
}
|
||
m_stage_use_dice = true;
|
||
m_role_order = {
|
||
BattleRole::Warrior, BattleRole::Pioneer, BattleRole::Medic, BattleRole::Tank, BattleRole::Sniper,
|
||
BattleRole::Caster, BattleRole::Support, BattleRole::Special, BattleRole::Drone,
|
||
};
|
||
}
|
||
m_wait_melee.assign(m_homes.size(), true);
|
||
m_wait_medic.assign(m_homes.size(), true);
|
||
m_indeed_no_medic.assign(m_homes.size(), false);
|
||
// 认为开局时未在所有路线放干员时为紧急状态,并把路线压入栈
|
||
// 由于开局路线设为0,故0不需要被压入栈
|
||
m_cur_home_index = 0;
|
||
m_is_cur_urgent = true;
|
||
for (size_t index = m_homes.size() - 1; index > 0; index--) {
|
||
m_next_urgent_index.push(index);
|
||
}
|
||
m_index_count.assign(m_homes.size(), 0);
|
||
if (opt && !opt->key_kills.empty()) {
|
||
std::string log_str = "[ ";
|
||
for (const auto& kills : opt->key_kills) {
|
||
m_key_kills.emplace(kills);
|
||
log_str += std::to_string(kills) + ", ";
|
||
}
|
||
log_str += "]";
|
||
Log.info("key kills:", log_str);
|
||
}
|
||
|
||
if (m_homes.empty()) {
|
||
Log.error("Unknown home pos");
|
||
}
|
||
|
||
if (calced) {
|
||
auto cb_info = basic_info_with_what("StageInfo");
|
||
auto& details = cb_info["details"];
|
||
details["name"] = m_stage_name;
|
||
details["size"] = m_side_tile_info.size();
|
||
callback(AsstMsg::SubTaskExtraInfo, cb_info);
|
||
}
|
||
else {
|
||
callback(AsstMsg::SubTaskExtraInfo, basic_info_with_what("StageInfoError"));
|
||
}
|
||
|
||
return calced;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::battle_pause()
|
||
{
|
||
return ProcessTask(*this, { "BattlePause" }).run();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::auto_battle()
|
||
{
|
||
LogTraceFunction;
|
||
|
||
// if (int hp = battle_analyzer.get_hp();
|
||
// hp != 0) {
|
||
// bool used_skills = false;
|
||
// if (hp < m_pre_hp) { // 说明漏怪了,漏怪就开技能(
|
||
// for (const Rect& rect : battle_analyzer.get_ready_skills()) {
|
||
// used_skills = true;
|
||
// if (!use_skill(rect)) {
|
||
// break;
|
||
// }
|
||
// }
|
||
// }
|
||
// m_pre_hp = hp;
|
||
// if (used_skills) {
|
||
// return true;
|
||
// }
|
||
// }
|
||
|
||
// 将存在于场上超过限时的召唤物所在的地块重新设为可用
|
||
Time_Point now_time = std::chrono::system_clock::now();
|
||
while ((!m_need_clear_tiles.empty()) && m_need_clear_tiles.top().placed_time < now_time) {
|
||
const auto& placed_loc = m_need_clear_tiles.top().placed_loc;
|
||
if (auto iter = m_used_tiles.find(placed_loc); iter != m_used_tiles.end()) {
|
||
m_opers_in_field.erase((iter->second).first);
|
||
m_used_tiles.erase(iter);
|
||
}
|
||
m_need_clear_tiles.pop();
|
||
}
|
||
|
||
const cv::Mat& image = m_ctrler->get_image();
|
||
if (try_possible_skill(image)) {
|
||
return true;
|
||
}
|
||
|
||
BattleImageAnalyzer battle_analyzer(image);
|
||
battle_analyzer.set_target(BattleImageAnalyzer::Target::Roguelike);
|
||
|
||
if (!battle_analyzer.analyze()) {
|
||
return false;
|
||
}
|
||
|
||
battle_analyzer.sort_opers_by_cost();
|
||
auto opers = battle_analyzer.get_opers();
|
||
if (opers.empty()) {
|
||
return true;
|
||
}
|
||
battle_analyzer.set_target(BattleImageAnalyzer::Target::Oper);
|
||
|
||
if (m_cur_home_index >= m_homes.size()) {
|
||
m_cur_home_index = 0;
|
||
}
|
||
|
||
size_t available_count = 0;
|
||
size_t cooling_count = 0;
|
||
std::vector<size_t> new_urgent;
|
||
std::vector<std::string> cooling_opers;
|
||
const auto use_oper_task_ptr = Task.get("BattleUseOper");
|
||
bool has_dice = false;
|
||
BattleRealTimeOper dice;
|
||
for (auto& oper : opers) {
|
||
if (oper.cooling) cooling_count++;
|
||
if (oper.available) available_count++;
|
||
if (oper.role == BattleRole::Drone) {
|
||
if (!m_dice_image.empty()) {
|
||
MatchImageAnalyzer dice_analyzer(image);
|
||
dice_analyzer.set_templ(m_dice_image);
|
||
if (dice_analyzer.analyze()) {
|
||
has_dice = true;
|
||
oper.name = "骰子";
|
||
dice = oper;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
bool can_use_dice = m_stage_use_dice && has_dice;
|
||
if (!can_use_dice) {
|
||
m_use_dice = false;
|
||
}
|
||
// 如果发现有新撤退的或者新转好cd的,就更新m_retreated_opers
|
||
if (cooling_count != m_retreated_opers.size()) {
|
||
const decltype(m_retreated_opers) ret_copy(m_retreated_opers);
|
||
m_retreated_opers.clear();
|
||
if (cooling_count > 0) {
|
||
battle_pause();
|
||
}
|
||
Rect cur_rect;
|
||
for (size_t i = 0; i < opers.size(); ++i) {
|
||
const auto& cur_opers = battle_analyzer.get_opers();
|
||
size_t offset = opers.size() > cur_opers.size() ? opers.size() - cur_opers.size() : 0;
|
||
const auto& oper = cur_opers.at(i - offset);
|
||
if ((!oper.cooling) || oper.role == BattleRole::Drone) {
|
||
continue;
|
||
}
|
||
cur_rect = oper.rect;
|
||
m_ctrler->click(cur_rect);
|
||
sleep(use_oper_task_ptr->pre_delay);
|
||
const cv::Mat& new_image = m_ctrler->get_image();
|
||
|
||
OcrWithPreprocessImageAnalyzer oper_name_analyzer(new_image);
|
||
oper_name_analyzer.set_task_info("BattleOperName");
|
||
oper_name_analyzer.set_replace(Task.get<OcrTaskInfo>("CharsNameOcrReplace")->replace_map);
|
||
|
||
std::string oper_name = "Unknown";
|
||
if (oper_name_analyzer.analyze()) {
|
||
oper_name_analyzer.sort_result_by_score();
|
||
oper_name = oper_name_analyzer.get_result().front().text;
|
||
}
|
||
|
||
battle_analyzer.set_image(new_image);
|
||
battle_analyzer.analyze();
|
||
battle_analyzer.sort_opers_by_cost();
|
||
|
||
if (oper_name == "Unknown") {
|
||
continue;
|
||
}
|
||
m_retreated_opers.emplace(oper_name);
|
||
|
||
if (ret_copy.contains(oper_name)) {
|
||
continue;
|
||
}
|
||
if (auto iter = m_opers_in_field.find(oper_name); iter == m_opers_in_field.end()) {
|
||
continue;
|
||
}
|
||
|
||
Log.info(oper_name, "retreated");
|
||
if (auto del_pos_melee = m_melee_for_home_index.find(iter->second);
|
||
del_pos_melee != m_melee_for_home_index.end()) {
|
||
m_wait_melee[del_pos_melee->second] = true;
|
||
new_urgent.emplace_back(del_pos_melee->second);
|
||
m_melee_for_home_index.erase(del_pos_melee);
|
||
}
|
||
else if (auto del_pos_medic = m_medic_for_home_index.find(iter->second);
|
||
del_pos_medic != m_medic_for_home_index.end()) {
|
||
for (const size_t& home_index : del_pos_medic->second) {
|
||
m_wait_medic[home_index] = true;
|
||
}
|
||
m_medic_for_home_index.erase(del_pos_medic);
|
||
}
|
||
m_used_tiles.erase(iter->second);
|
||
m_opers_in_field.erase(iter);
|
||
}
|
||
if (cooling_count > 0) {
|
||
m_ctrler->click(cur_rect);
|
||
battle_pause();
|
||
}
|
||
}
|
||
if (!new_urgent.empty()) {
|
||
// 出现新的紧急情况,立即切到这条线路,并把其他紧急情况压入栈
|
||
Log.info("New urgent situation detected");
|
||
if (m_is_cur_urgent) {
|
||
m_next_urgent_index.push(m_cur_home_index);
|
||
}
|
||
else {
|
||
m_last_not_urgent = static_cast<int>(m_cur_home_index);
|
||
}
|
||
m_cur_home_index = new_urgent.at(0);
|
||
for (size_t i = 1; i < new_urgent.size(); ++i) {
|
||
m_next_urgent_index.push(new_urgent.at(i));
|
||
}
|
||
if (can_use_dice) {
|
||
// 先放骰子,再放近战
|
||
m_use_dice = true;
|
||
m_next_urgent_index.push(new_urgent.at(0));
|
||
}
|
||
}
|
||
else if ((!m_is_cur_urgent || m_index_count[m_cur_home_index] == 0) && m_used_tiles.size() >= 2) {
|
||
// 超过一半的人费用都没好,且没有紧急情况,那就不下人
|
||
size_t not_cooling_count = opers.size() - cooling_count;
|
||
if (available_count <= not_cooling_count / 2) {
|
||
Log.trace("already used", m_used_tiles.size(), ", now_total", opers.size(), ", available", available_count,
|
||
", not_cooling", not_cooling_count);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
const auto swipe_oper_task_ptr = Task.get("BattleSwipeOper");
|
||
|
||
// 点击当前最合适的干员
|
||
BattleRealTimeOper opt_oper;
|
||
bool oper_found = false;
|
||
|
||
bool has_melee = false;
|
||
bool has_medic = false;
|
||
bool wait_melee = m_wait_melee[m_cur_home_index];
|
||
bool wait_medic = m_wait_medic[m_cur_home_index];
|
||
if (m_use_dice) {
|
||
opt_oper = std::move(dice);
|
||
oper_found = true;
|
||
if (available_locations(opt_oper.role).empty()) {
|
||
Log.info("Tiles full");
|
||
return true;
|
||
}
|
||
}
|
||
else {
|
||
// 对于每个蓝门,先下个地面单位(如果有的话)
|
||
// 第二个人下奶(如果有的话)
|
||
for (const auto& op : opers) {
|
||
if ((op.role == BattleRole::Warrior || op.role == BattleRole::Pioneer || op.role == BattleRole::Tank) &&
|
||
(!op.cooling)) {
|
||
has_melee = true;
|
||
}
|
||
else if (op.role == BattleRole::Medic && (!op.cooling)) {
|
||
has_medic = true;
|
||
}
|
||
}
|
||
|
||
wait_melee &= has_melee;
|
||
wait_medic &= has_medic;
|
||
for (auto role : m_role_order) {
|
||
if (wait_melee) {
|
||
if (role != BattleRole::Warrior && role != BattleRole::Pioneer && role != BattleRole::Tank) {
|
||
continue;
|
||
}
|
||
}
|
||
else if (wait_medic) {
|
||
if (role != BattleRole::Medic) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
for (const auto& oper : opers) {
|
||
if (!oper.available) {
|
||
continue;
|
||
}
|
||
if (oper.name == "骰子") {
|
||
continue;
|
||
}
|
||
if (oper.role == role) {
|
||
opt_oper = oper;
|
||
oper_found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (oper_found) {
|
||
break;
|
||
}
|
||
}
|
||
if (!oper_found) {
|
||
return true;
|
||
}
|
||
|
||
// 预计算干员是否有点地方放
|
||
if (available_locations(opt_oper.role).empty()) {
|
||
Log.info("Tiles full");
|
||
return true;
|
||
}
|
||
|
||
if (m_first_deploy) {
|
||
m_first_deploy = false;
|
||
battle_pause();
|
||
Rect cur_rect;
|
||
for (size_t i = 0; i < opers.size(); ++i) {
|
||
const auto& cur_opers = battle_analyzer.get_opers();
|
||
size_t offset = opers.size() > cur_opers.size() ? opers.size() - cur_opers.size() : 0;
|
||
const auto& oper = cur_opers.at(i - offset);
|
||
if (oper.role == BattleRole::Drone) {
|
||
cur_rect = oper.rect;
|
||
m_ctrler->click(cur_rect);
|
||
sleep(use_oper_task_ptr->pre_delay);
|
||
const cv::Mat& new_image = m_ctrler->get_image();
|
||
|
||
OcrWithPreprocessImageAnalyzer oper_name_analyzer(new_image);
|
||
oper_name_analyzer.set_task_info("BattleOperName");
|
||
oper_name_analyzer.set_replace(Task.get<OcrTaskInfo>("CharsNameOcrReplace")->replace_map);
|
||
|
||
battle_analyzer.set_image(new_image);
|
||
battle_analyzer.analyze();
|
||
battle_analyzer.sort_opers_by_cost();
|
||
|
||
if (!oper_name_analyzer.analyze()) continue;
|
||
oper_name_analyzer.sort_result_by_score();
|
||
if (oper_name_analyzer.get_result().front().text == "骰子") {
|
||
m_dice_image = opers.at(i).avatar;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!cur_rect.empty()) {
|
||
m_ctrler->click(cur_rect);
|
||
}
|
||
battle_pause();
|
||
}
|
||
|
||
m_ctrler->click(opt_oper.rect);
|
||
sleep(use_oper_task_ptr->pre_delay);
|
||
|
||
OcrWithPreprocessImageAnalyzer oper_name_analyzer(m_ctrler->get_image());
|
||
oper_name_analyzer.set_task_info("BattleOperName");
|
||
oper_name_analyzer.set_replace(Task.get<OcrTaskInfo>("CharsNameOcrReplace")->replace_map);
|
||
|
||
opt_oper.name = "Unknown";
|
||
if (oper_name_analyzer.analyze()) {
|
||
oper_name_analyzer.sort_result_by_score();
|
||
opt_oper.name = oper_name_analyzer.get_result().front().text;
|
||
}
|
||
}
|
||
|
||
// 计算最优部署位置及方向
|
||
const auto& [placed_loc, direction] = calc_best_plan(opt_oper);
|
||
|
||
// 将干员拖动到场上
|
||
Point placed_point = m_side_tile_info.at(placed_loc).pos;
|
||
Rect placed_rect(placed_point.x, placed_point.y, 1, 1);
|
||
int dist = static_cast<int>(Point::distance(
|
||
placed_point, { opt_oper.rect.x + opt_oper.rect.width / 2, opt_oper.rect.y + opt_oper.rect.height / 2 }));
|
||
// 1000 是随便取的一个系数,把整数的 pre_delay 转成小数用的
|
||
int duration = static_cast<int>(swipe_oper_task_ptr->pre_delay / 800.0 * dist * log10(dist));
|
||
m_ctrler->swipe(opt_oper.rect, placed_rect, duration, true, 0);
|
||
sleep(use_oper_task_ptr->rear_delay);
|
||
|
||
// 将方向转换为实际的 swipe end 坐标点
|
||
constexpr int coeff = 500;
|
||
Point end_point = placed_point + (direction * coeff);
|
||
|
||
m_ctrler->swipe(placed_point, end_point, swipe_oper_task_ptr->rear_delay, true, 100);
|
||
|
||
if (opt_oper.role == BattleRole::Drone) {
|
||
cancel_oper_selection();
|
||
now_time = std::chrono::system_clock::now();
|
||
if (opt_oper.name == "骰子") {
|
||
m_need_clear_tiles.emplace(now_time + std::chrono::seconds(20), placed_loc);
|
||
}
|
||
else {
|
||
m_need_clear_tiles.emplace(now_time + std::chrono::seconds(35), placed_loc);
|
||
}
|
||
}
|
||
|
||
Log.info("Try to deploy oper", opt_oper.name);
|
||
m_used_tiles.emplace(placed_loc, std::make_pair(opt_oper.name, opt_oper.role));
|
||
m_opers_in_field.emplace(opt_oper.name, placed_loc);
|
||
m_opers_used = true;
|
||
if (wait_melee) {
|
||
m_wait_melee[m_cur_home_index] = false;
|
||
m_melee_for_home_index.emplace(placed_loc, m_cur_home_index);
|
||
}
|
||
else if (wait_medic) {
|
||
// 两次轮到wait_medic的轮次时仍没有医疗干员奶当前位置,即认为奶不到
|
||
if (m_wait_medic[m_cur_home_index]) {
|
||
m_indeed_no_medic[m_cur_home_index] = false;
|
||
}
|
||
else {
|
||
if (m_indeed_no_medic[m_cur_home_index]) {
|
||
m_wait_medic[m_cur_home_index] = false;
|
||
}
|
||
else {
|
||
m_indeed_no_medic[m_cur_home_index] = true;
|
||
}
|
||
}
|
||
}
|
||
// 第一次在这条路线上放近战干员时,应当保证有一半干员费用转好
|
||
m_index_count[m_cur_home_index]++;
|
||
// 如果有紧急需要部署的路线,那么下一条路线设为它
|
||
if (m_next_urgent_index.empty()) {
|
||
if (m_last_not_urgent != -1) {
|
||
// 从上一次不是紧急的下一条路线开始
|
||
m_cur_home_index = static_cast<size_t>(m_last_not_urgent) + 1;
|
||
m_last_not_urgent = -1;
|
||
}
|
||
else {
|
||
++m_cur_home_index;
|
||
}
|
||
m_is_cur_urgent = false;
|
||
}
|
||
else {
|
||
if (m_last_not_urgent == -1) {
|
||
m_last_not_urgent = static_cast<int>(m_cur_home_index);
|
||
}
|
||
m_is_cur_urgent = true;
|
||
m_cur_home_index = m_next_urgent_index.top();
|
||
Log.info("Enter urgent situtation, to path", m_cur_home_index);
|
||
m_next_urgent_index.pop();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::speed_up()
|
||
{
|
||
return ProcessTask(*this, { "Roguelike1BattleSpeedUp" }).run();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::use_skill(const Rect& rect)
|
||
{
|
||
m_ctrler->click(rect);
|
||
sleep(Task.get("BattleUseOper")->pre_delay);
|
||
|
||
ProcessTask task(*this, { "BattleUseSkillJustClick" });
|
||
task.set_retry_times(0);
|
||
return task.run();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::retreat(const Point& point)
|
||
{
|
||
m_ctrler->click(point);
|
||
sleep(Task.get("BattleUseOper")->pre_delay);
|
||
|
||
return ProcessTask(*this, { "BattleOperRetreatJustClick" }).run();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::abandon()
|
||
{
|
||
return ProcessTask(*this, { "Roguelike1BattleExitBegin" }).run();
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::all_melee_retreat()
|
||
{
|
||
for (const auto& loc : m_used_tiles | views::keys) {
|
||
auto& tile_info = m_normal_tile_info[loc];
|
||
auto& type = tile_info.buildable;
|
||
if (type == Loc::Melee || type == Loc::All) {
|
||
retreat(tile_info.pos);
|
||
}
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::clear()
|
||
{
|
||
m_opers_used = false;
|
||
m_pre_hp = 0;
|
||
m_homes.clear();
|
||
m_wait_melee.clear();
|
||
m_wait_medic.clear();
|
||
m_indeed_no_medic.clear();
|
||
m_melee_for_home_index.clear();
|
||
m_medic_for_home_index.clear();
|
||
decltype(m_next_urgent_index) empty_stack;
|
||
m_next_urgent_index.swap(empty_stack);
|
||
m_is_cur_urgent = false;
|
||
m_last_not_urgent = -1;
|
||
m_index_count.clear();
|
||
m_blacklist_location.clear();
|
||
m_retreated_opers.clear();
|
||
decltype(m_need_clear_tiles) empty_heap;
|
||
m_need_clear_tiles.swap(empty_heap);
|
||
decltype(m_key_kills) empty_queue;
|
||
m_key_kills.swap(empty_queue);
|
||
m_cur_home_index = 0;
|
||
m_stage_name.clear();
|
||
m_side_tile_info.clear();
|
||
m_used_tiles.clear();
|
||
m_opers_in_field.clear();
|
||
m_kills = 0;
|
||
m_total_kills = 0;
|
||
m_use_dice = false;
|
||
m_stage_use_dice = true;
|
||
m_dice_image = cv::Mat();
|
||
m_first_deploy = true;
|
||
|
||
for (auto& [key, status] : m_restore_status) {
|
||
m_status->set_number(key, status);
|
||
}
|
||
m_restore_status.clear();
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::try_possible_skill(const cv::Mat& image)
|
||
{
|
||
if (!check_key_kills(image)) {
|
||
return false;
|
||
}
|
||
|
||
auto task_ptr = Task.get("BattleAutoSkillFlag");
|
||
const Rect& skill_roi_move = task_ptr->rect_move;
|
||
|
||
MatchImageAnalyzer analyzer(image);
|
||
analyzer.set_task_info(task_ptr);
|
||
bool used = false;
|
||
for (auto& [loc, oper_pair] : m_used_tiles) {
|
||
std::string status_key = "RoguelikeSkillUsage-" + oper_pair.first;
|
||
auto usage = BattleSkillUsage::Possibly;
|
||
auto usage_opt = m_status->get_number(status_key);
|
||
if (usage_opt) {
|
||
usage = static_cast<BattleSkillUsage>(usage_opt.value());
|
||
}
|
||
|
||
if (usage != BattleSkillUsage::Possibly && usage != BattleSkillUsage::Once) {
|
||
continue;
|
||
}
|
||
const Point pos = m_normal_tile_info.at(loc).pos;
|
||
const Rect pos_rect(pos.x, pos.y, 1, 1);
|
||
const Rect roi = pos_rect.move(skill_roi_move);
|
||
analyzer.set_roi(roi);
|
||
if (!analyzer.analyze()) {
|
||
continue;
|
||
}
|
||
m_ctrler->click(pos_rect);
|
||
sleep(Task.get("BattleUseOper")->pre_delay);
|
||
bool ret = ProcessTask(*this, { "BattleSkillReadyOnClick" }).set_retry_times(2).run();
|
||
if (!ret) {
|
||
cancel_oper_selection();
|
||
}
|
||
used |= ret;
|
||
if (usage == BattleSkillUsage::Once) {
|
||
m_status->set_number(status_key, static_cast<int64_t>(BattleSkillUsage::OnceUsed));
|
||
m_restore_status[status_key] = static_cast<int64_t>(BattleSkillUsage::Once);
|
||
}
|
||
}
|
||
return used;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::check_key_kills(const cv::Mat& image)
|
||
{
|
||
if (m_key_kills.empty()) {
|
||
return true;
|
||
}
|
||
int need_kills = m_key_kills.front();
|
||
|
||
BattleImageAnalyzer analyzer(image);
|
||
if (m_total_kills) {
|
||
analyzer.set_pre_total_kills(m_total_kills);
|
||
}
|
||
analyzer.set_target(BattleImageAnalyzer::Target::Kills);
|
||
if (analyzer.analyze()) {
|
||
m_kills = analyzer.get_kills();
|
||
m_total_kills = analyzer.get_total_kills();
|
||
if (m_kills >= need_kills) {
|
||
m_key_kills.pop();
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::wait_start()
|
||
{
|
||
auto start_time = std::chrono::system_clock::now();
|
||
auto check_time = [&]() -> bool {
|
||
using namespace std::chrono_literals;
|
||
return std::chrono::system_clock::now() - start_time > 1min;
|
||
};
|
||
|
||
MatchImageAnalyzer officially_begin_analyzer;
|
||
officially_begin_analyzer.set_task_info("BattleOfficiallyBegin");
|
||
cv::Mat image;
|
||
while (!need_exit() && !check_time()) {
|
||
image = m_ctrler->get_image();
|
||
officially_begin_analyzer.set_image(image);
|
||
if (officially_begin_analyzer.analyze()) {
|
||
break;
|
||
}
|
||
std::this_thread::yield();
|
||
}
|
||
|
||
BattleImageAnalyzer oper_analyzer;
|
||
oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper);
|
||
while (!need_exit() && !check_time()) {
|
||
image = m_ctrler->get_image();
|
||
oper_analyzer.set_image(image);
|
||
if (oper_analyzer.analyze()) {
|
||
break;
|
||
}
|
||
std::this_thread::yield();
|
||
}
|
||
|
||
std::filesystem::create_directory("map");
|
||
for (const auto& [loc, info] : m_normal_tile_info) {
|
||
std::string text = "( " + std::to_string(loc.x) + ", " + std::to_string(loc.y) + " )";
|
||
cv::putText(image, text, cv::Point(info.pos.x - 30, info.pos.y), 1, 1.2, cv::Scalar(0, 0, 255), 2);
|
||
}
|
||
|
||
// 识别一帧总击杀数
|
||
BattleImageAnalyzer kills_analyzer(image);
|
||
kills_analyzer.set_target(BattleImageAnalyzer::Target::Kills);
|
||
if (kills_analyzer.analyze()) {
|
||
m_kills = kills_analyzer.get_kills();
|
||
m_total_kills = kills_analyzer.get_total_kills();
|
||
}
|
||
|
||
asst::imwrite("map/" + m_stage_name + ".png", image);
|
||
return true;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::cancel_oper_selection()
|
||
{
|
||
return ProcessTask(*this, { "BattleCancelSelection" }).run();
|
||
}
|
||
|
||
std::vector<asst::Point> asst::RoguelikeBattleTaskPlugin::available_locations(BattleRole role)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
auto buildable_type = Loc::All;
|
||
switch (role) {
|
||
case BattleRole::Medic:
|
||
case BattleRole::Support:
|
||
case BattleRole::Sniper:
|
||
case BattleRole::Caster:
|
||
buildable_type = Loc::Ranged;
|
||
break;
|
||
case BattleRole::Pioneer:
|
||
case BattleRole::Warrior:
|
||
case BattleRole::Tank:
|
||
buildable_type = Loc::Melee;
|
||
break;
|
||
case BattleRole::Special:
|
||
case BattleRole::Drone:
|
||
default:
|
||
//// 特种和无人机,有的只能放地面,有的又只能放高台,不好判断
|
||
//// 笨办法,都试试,总有一次能成的
|
||
//{
|
||
// static Loc static_loc = Loc::Melee;
|
||
// loc = static_loc;
|
||
// if (static_loc == Loc::Melee) {
|
||
// static_loc = Loc::Ranged;
|
||
// }
|
||
// else {
|
||
// static_loc = Loc::Melee;
|
||
// }
|
||
//}
|
||
// 大部分无人机都是可以摆在地上的,摆烂了
|
||
buildable_type = Loc::Melee;
|
||
break;
|
||
}
|
||
|
||
std::vector<Point> result;
|
||
for (const auto& [loc, tile] : m_normal_tile_info) {
|
||
if ((tile.buildable == buildable_type || tile.buildable == Loc::All) &&
|
||
tile.key != TilePack::TileKey::DeepSea && // 水上要先放板子才能放人,肉鸽里也没板子,那就当作不可放置
|
||
!m_used_tiles.contains(loc) && !m_blacklist_location.contains(loc)) {
|
||
result.emplace_back(loc);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
asst::BattleAttackRange asst::RoguelikeBattleTaskPlugin::get_attack_range(const BattleRealTimeOper& oper)
|
||
{
|
||
int64_t elite = m_status->get_number(RuntimeStatus::RoguelikeCharElitePrefix + oper.name).value_or(0);
|
||
BattleAttackRange right_attack_range = BattleData.get_range(oper.name, elite);
|
||
|
||
if (right_attack_range == BattleDataConfiger::EmptyRange) {
|
||
switch (oper.role) {
|
||
case BattleRole::Support:
|
||
right_attack_range = {
|
||
Point(-1, -1), Point(0, -1), Point(1, -1), Point(2, -1), //
|
||
Point(-1, 0), Point(0, 0), Point(1, 0), Point(2, 0), //
|
||
Point(-1, 1), Point(0, 1), Point(1, 1), Point(2, 1), //
|
||
};
|
||
break;
|
||
case BattleRole::Caster:
|
||
right_attack_range = {
|
||
Point(0, -1), Point(1, -1), Point(2, -1), //
|
||
Point(0, 0), Point(1, 0), Point(2, 0), Point(3, 0), //
|
||
Point(0, 1), Point(1, 1), Point(2, 1), //
|
||
};
|
||
break;
|
||
case BattleRole::Medic:
|
||
case BattleRole::Sniper:
|
||
right_attack_range = {
|
||
Point(0, -1), Point(1, -1), Point(2, -1), Point(3, -1), //
|
||
Point(0, 0), Point(1, 0), Point(2, 0), Point(3, 0), //
|
||
Point(0, 1), Point(1, 1), Point(2, 1), Point(3, 1), //
|
||
};
|
||
break;
|
||
case BattleRole::Warrior:
|
||
right_attack_range = { Point(0, 0), Point(1, 0), Point(2, 0) };
|
||
break;
|
||
case BattleRole::Special:
|
||
case BattleRole::Tank:
|
||
case BattleRole::Pioneer:
|
||
case BattleRole::Drone:
|
||
right_attack_range = { Point(0, 0), Point(1, 0) };
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
return right_attack_range;
|
||
}
|
||
|
||
asst::RoguelikeBattleTaskPlugin::DeployInfo asst::RoguelikeBattleTaskPlugin::calc_best_plan(
|
||
const BattleRealTimeOper& oper)
|
||
{
|
||
if (m_cur_home_index >= m_homes.size()) {
|
||
m_cur_home_index = 0;
|
||
}
|
||
|
||
Point home(5, 5); // 实在找不到家门了,随便取个点当家门用算了,一般是地图的中间
|
||
Point recommended_direction;
|
||
if (m_cur_home_index < m_homes.size()) {
|
||
const auto& rp_home = m_homes.at(m_cur_home_index);
|
||
home = rp_home.location;
|
||
static const std::unordered_map<BattleDeployDirection, Point> direction_map = {
|
||
{ BattleDeployDirection::Up, Point::up() }, { BattleDeployDirection::Down, Point::down() },
|
||
{ BattleDeployDirection::Left, Point::left() }, { BattleDeployDirection::Right, Point::right() },
|
||
{ BattleDeployDirection::None, Point() },
|
||
};
|
||
recommended_direction = direction_map.at(rp_home.direction);
|
||
}
|
||
|
||
auto comp_dist = [&](const Point& lhs, const Point& rhs) -> bool {
|
||
int lhs_y_dist = std::abs(lhs.y - home.y);
|
||
int lhs_dist = std::abs(lhs.x - home.x) + lhs_y_dist;
|
||
int rhs_y_dist = std::abs(rhs.y - home.y);
|
||
int rhs_dist = std::abs(rhs.x - home.x) + rhs_y_dist;
|
||
// 距离一样选择 x 轴上的,因为一般的地图都是横向的长方向
|
||
return lhs_dist == rhs_dist ? lhs_y_dist < rhs_y_dist : lhs_dist < rhs_dist;
|
||
};
|
||
|
||
std::vector<Point> available_loc = available_locations(oper.role);
|
||
|
||
// 把所有可用的点按距离排个序
|
||
ranges::sort(available_loc, comp_dist);
|
||
|
||
if (oper.name == "骰子") {
|
||
return { available_loc.back(), Point::right() };
|
||
}
|
||
|
||
// 取距离最近的N个点,计算分数。然后使用得分最高的点
|
||
constexpr int CalcPointCount = 4;
|
||
if (available_loc.size() > CalcPointCount) {
|
||
available_loc.erase(available_loc.begin() + CalcPointCount, available_loc.end());
|
||
}
|
||
|
||
Point best_location;
|
||
Point best_direction;
|
||
int max_score = INT_MIN;
|
||
|
||
const auto& near_loc = available_loc.at(0);
|
||
int min_dist = std::abs(near_loc.x - home.x) + std::abs(near_loc.y - home.y);
|
||
|
||
for (const auto& loc : available_loc) {
|
||
auto cur_result = calc_best_direction_and_score(loc, oper, recommended_direction);
|
||
// 离得远的要扣分
|
||
constexpr int DistWeights = -1050;
|
||
int extra_dist = std::abs(loc.x - home.x) + std::abs(loc.y - home.y) - min_dist;
|
||
int extra_dist_score = DistWeights * extra_dist;
|
||
if (oper.role == BattleRole::Medic) { // 医疗干员离得远无所谓
|
||
extra_dist_score = 0;
|
||
}
|
||
|
||
if (cur_result.second + extra_dist_score > max_score) {
|
||
max_score = cur_result.second + extra_dist_score;
|
||
best_location = loc;
|
||
best_direction = cur_result.first;
|
||
}
|
||
}
|
||
|
||
// 如果是医疗干员,判断覆盖范围内有无第一次放置的干员
|
||
if (oper.role == BattleRole::Medic) {
|
||
BattleAttackRange right_attack_range = get_attack_range(oper);
|
||
for (const Point& direction : { Point::right(), Point::up(), Point::left(), Point::down() }) {
|
||
if (direction == best_direction) break;
|
||
for (Point& point : right_attack_range)
|
||
point = { point.y, -point.x };
|
||
}
|
||
std::vector<size_t> contain_index;
|
||
for (const Point& relative_pos : right_attack_range) {
|
||
Point absolute_pos = best_location + relative_pos;
|
||
if (auto iter = m_melee_for_home_index.find(absolute_pos); iter != m_melee_for_home_index.end()) {
|
||
m_wait_medic[iter->second] = false;
|
||
contain_index.emplace_back(iter->second);
|
||
}
|
||
}
|
||
if (!contain_index.empty()) {
|
||
m_medic_for_home_index.emplace(best_location, std::move(contain_index));
|
||
}
|
||
}
|
||
|
||
return { best_location, best_direction };
|
||
}
|
||
|
||
std::pair<asst::Point, int> asst::RoguelikeBattleTaskPlugin::calc_best_direction_and_score(
|
||
Point loc, const BattleRealTimeOper& oper, Point recommended_direction)
|
||
{
|
||
LogTraceFunction;
|
||
|
||
// 根据家门的方向计算一下大概的朝向
|
||
if (m_cur_home_index >= m_homes.size()) {
|
||
m_cur_home_index = 0;
|
||
}
|
||
Point home_loc(5, 5);
|
||
if (m_cur_home_index < m_homes.size()) {
|
||
home_loc = m_homes.at(m_cur_home_index).location;
|
||
}
|
||
|
||
auto sgn = [](const int& x) -> int {
|
||
if (x > 0) return 1;
|
||
if (x < 0) return -1;
|
||
return 0;
|
||
};
|
||
|
||
Point base_direction(0, 0);
|
||
if (loc.x == home_loc.x) {
|
||
base_direction.y = sgn(loc.y - home_loc.y);
|
||
}
|
||
else {
|
||
base_direction.x = sgn(loc.x - home_loc.x);
|
||
}
|
||
Point home_direction(-base_direction.x, -base_direction.y);
|
||
// 医疗反着算
|
||
if (oper.role == BattleRole::Medic) {
|
||
base_direction = -base_direction;
|
||
}
|
||
|
||
// 按朝右算,后面根据方向做转换
|
||
BattleAttackRange right_attack_range = get_attack_range(oper);
|
||
|
||
int max_score = 0;
|
||
Point opt_direction;
|
||
|
||
for (const Point& direction : { Point::right(), Point::up(), Point::left(), Point::down() }) {
|
||
int score = 0;
|
||
for (const Point& relative_pos : right_attack_range) {
|
||
Point absolute_pos = loc + relative_pos;
|
||
|
||
using TileKey = TilePack::TileKey;
|
||
// 战斗干员朝向的权重
|
||
static const std::unordered_map<TileKey, int> TileKeyFightWeights = {
|
||
{ TileKey::Invalid, 0 }, { TileKey::Forbidden, 0 }, { TileKey::Wall, 500 },
|
||
{ TileKey::Road, 1000 }, { TileKey::Home, 500 }, { TileKey::EnemyHome, 1000 },
|
||
{ TileKey::Airport, 1000 }, { TileKey::Floor, 1000 }, { TileKey::Hole, 0 },
|
||
{ TileKey::Telin, 700 }, { TileKey::Telout, 800 }, { TileKey::Grass, 500 },
|
||
{ TileKey::DeepSea, 1000 }, { TileKey::Volcano, 1000 }, { TileKey::Healing, 1000 },
|
||
{ TileKey::Fence, 800 },
|
||
};
|
||
// 治疗干员朝向的权重
|
||
static const std::unordered_map<TileKey, int> TileKeyMedicWeights = {
|
||
{ TileKey::Invalid, 0 }, { TileKey::Forbidden, 0 }, { TileKey::Wall, 1000 },
|
||
{ TileKey::Road, 1000 }, { TileKey::Home, 0 }, { TileKey::EnemyHome, 0 },
|
||
{ TileKey::Airport, 0 }, { TileKey::Floor, 0 }, { TileKey::Hole, 0 },
|
||
{ TileKey::Telin, 0 }, { TileKey::Telout, 0 }, { TileKey::Grass, 500 },
|
||
{ TileKey::DeepSea, 0 }, { TileKey::Volcano, 1000 }, { TileKey::Healing, 1000 },
|
||
{ TileKey::Fence, 1000 },
|
||
};
|
||
|
||
switch (oper.role) {
|
||
case BattleRole::Medic:
|
||
if (m_used_tiles.contains(absolute_pos) &&
|
||
m_used_tiles[absolute_pos].second != BattleRole::Drone) // 根据哪个方向上人多决定朝向哪
|
||
score += 10000;
|
||
if (auto iter = m_side_tile_info.find(absolute_pos); iter != m_side_tile_info.end())
|
||
score += TileKeyMedicWeights.at(iter->second.key);
|
||
break;
|
||
default:
|
||
if (auto iter = m_side_tile_info.find(absolute_pos); iter != m_side_tile_info.end())
|
||
score += TileKeyFightWeights.at(iter->second.key);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (direction == base_direction) {
|
||
score += 300;
|
||
}
|
||
|
||
if (oper.role != BattleRole::Medic && direction == home_direction) {
|
||
score -= 500;
|
||
}
|
||
|
||
if (oper.role != BattleRole::Medic && direction == recommended_direction) {
|
||
score += 2000;
|
||
}
|
||
|
||
if (score > max_score) {
|
||
max_score = score;
|
||
opt_direction = direction;
|
||
}
|
||
|
||
// rotate relative attack range counterclockwise
|
||
for (Point& point : right_attack_range)
|
||
point = { point.y, -point.x };
|
||
}
|
||
|
||
return std::make_pair(opt_direction, max_score);
|
||
}
|