mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-18 10:10:45 +08:00
1010 lines
37 KiB
C++
1010 lines
37 KiB
C++
#include "RoguelikeBattleTaskPlugin.h"
|
||
|
||
#include "Utils/Ranges.hpp"
|
||
#include <chrono>
|
||
#include <future>
|
||
#include <vector>
|
||
|
||
#include "Utils/NoWarningCV.h"
|
||
|
||
#include "Config/Miscellaneous/BattleDataConfig.h"
|
||
#include "Config/Miscellaneous/TilePack.h"
|
||
#include "Config/Roguelike/RoguelikeCopilotConfig.h"
|
||
#include "Config/Roguelike/RoguelikeRecruitConfig.h"
|
||
#include "Config/TaskData.h"
|
||
#include "Controller/Controller.h"
|
||
#include "Status.h"
|
||
#include "Task/ProcessTask.h"
|
||
#include "Utils/ImageIo.hpp"
|
||
#include "Utils/Logger.hpp"
|
||
#include "Vision/Matcher.h"
|
||
#include "Vision/RegionOCRer.h"
|
||
|
||
using namespace asst::battle;
|
||
using namespace asst::battle::roguelike;
|
||
|
||
asst::RoguelikeBattleTaskPlugin::RoguelikeBattleTaskPlugin(const AsstCallback& callback, Assistant* inst,
|
||
std::string_view task_chain)
|
||
: AbstractTaskPlugin(callback, inst, task_chain), BattleHelper(inst)
|
||
{}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::verify(AsstMsg msg, const json::value& details) const
|
||
{
|
||
if (msg != AsstMsg::SubTaskCompleted || details.get("subtask", std::string()) != "ProcessTask") {
|
||
return false;
|
||
}
|
||
|
||
auto roguelike_name_opt = status()->get_properties(Status::RoguelikeTheme);
|
||
if (!roguelike_name_opt) {
|
||
Log.error("Roguelike name doesn't exist!");
|
||
return false;
|
||
}
|
||
const std::string roguelike_name = std::move(roguelike_name_opt.value()) + "@";
|
||
const std::string& task = details.get("details", "task", "");
|
||
std::string_view task_view = task;
|
||
if (task_view.starts_with(roguelike_name)) {
|
||
task_view.remove_prefix(roguelike_name.length());
|
||
}
|
||
if (task_view == "Roguelike@StartAction") {
|
||
return true;
|
||
}
|
||
else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::_run()
|
||
{
|
||
using namespace std::chrono_literals;
|
||
LogTraceFunction;
|
||
|
||
if (!calc_stage_info()) {
|
||
return false;
|
||
}
|
||
update_deployment(true);
|
||
speed_up();
|
||
|
||
bool timeout = false;
|
||
auto start_time = std::chrono::steady_clock::now();
|
||
while (!need_exit()) {
|
||
// 不在战斗场景,且已使用过了干员,说明已经打完了,就结束循环
|
||
if (!do_once() && !m_first_deploy) {
|
||
break;
|
||
}
|
||
|
||
auto duration = std::chrono::steady_clock::now() - start_time;
|
||
if (!timeout && duration > 8min) {
|
||
timeout = true;
|
||
Log.info("Timeout, retreat!");
|
||
// 超时了,一般是某个怪被干员卡住了,一直不结束。
|
||
all_melee_retreat();
|
||
}
|
||
else if (timeout && duration > 10min) {
|
||
Log.info("Timeout again, abandon!");
|
||
// 超时撤退了还一直卡着,只能放弃了
|
||
abandon();
|
||
break;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::wait_until_start_button_clicked()
|
||
{
|
||
std::string theme = status()->get_properties(Status::RoguelikeTheme).value();
|
||
ProcessTask(*this, { theme + "@Roguelike@WaitForStartButtonClicked" }).set_task_delay(0).set_retry_times(0).run();
|
||
}
|
||
|
||
std::string asst::RoguelikeBattleTaskPlugin::oper_name_in_config(const battle::DeploymentOper& oper) const
|
||
{
|
||
if (oper.role == Role::Warrior && oper.name == "阿米娅") {
|
||
return "阿米娅-WARRIOR"; // 在 BattleData.json 中有
|
||
}
|
||
else {
|
||
return oper.name;
|
||
}
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::calc_stage_info()
|
||
{
|
||
LogTraceFunction;
|
||
|
||
wait_until_start_button_clicked();
|
||
clear();
|
||
|
||
bool calced = false;
|
||
|
||
const auto stage_name_task_ptr = Task.get("BattleStageName");
|
||
sleep(stage_name_task_ptr->pre_delay);
|
||
|
||
constexpr int StageNameRetryTimes = 100;
|
||
for (int i = 0; i != StageNameRetryTimes; ++i) {
|
||
if (need_exit()) {
|
||
return false;
|
||
}
|
||
RegionOCRer name_analyzer(ctrler()->get_image());
|
||
name_analyzer.set_task_info(stage_name_task_ptr);
|
||
if (!name_analyzer.analyze()) {
|
||
continue;
|
||
}
|
||
const std::string& text = name_analyzer.get_result().text;
|
||
if (text.empty()) {
|
||
continue;
|
||
}
|
||
|
||
static const std::vector<std::string> RoguelikeStageCode = { "ISW-NO", "ISW-DF", "ISW-DU", "ISW-SP",
|
||
std::string() };
|
||
TilePack::LevelKey stage_key;
|
||
stage_key.name = text;
|
||
|
||
for (const std::string& code : RoguelikeStageCode) {
|
||
stage_key.code = code;
|
||
if (!Tile.find(stage_key)) {
|
||
continue;
|
||
}
|
||
calced = true;
|
||
m_stage_name = text;
|
||
m_normal_tile_info = Tile.calc(stage_key, false);
|
||
m_side_tile_info = Tile.calc(stage_key, true);
|
||
break;
|
||
}
|
||
|
||
if (calced) {
|
||
break;
|
||
}
|
||
std::this_thread::yield();
|
||
}
|
||
|
||
if (!calced) {
|
||
callback(AsstMsg::SubTaskExtraInfo, basic_info_with_what("StageInfoError"));
|
||
return false;
|
||
}
|
||
|
||
auto opt = RoguelikeCopilot.get_stage_data(m_stage_name);
|
||
if (opt) {
|
||
m_homes = opt->replacement_home;
|
||
m_allow_to_use_dice = opt->use_dice_stage;
|
||
m_blacklist_location = opt->blacklist_location;
|
||
m_role_order = opt->role_order;
|
||
m_force_deploy_direction = opt->force_deploy_direction;
|
||
m_force_air_defense = AirDefenseData { .stop_blocking_deploy_num = opt->stop_deploy_blocking_num,
|
||
.deploy_air_defense_num = opt->force_deploy_air_defense_num,
|
||
.ban_medic = opt->force_ban_medic };
|
||
m_deploy_plan = opt->deploy_plan;
|
||
m_retreat_plan = opt->retreat_plan;
|
||
}
|
||
if (m_homes.empty()) {
|
||
for (const auto& [loc, side] : m_normal_tile_info) {
|
||
if (side.key == TilePack::TileKey::Home) {
|
||
m_homes.emplace_back(ReplacementHome { loc, battle::DeployDirection::None });
|
||
}
|
||
}
|
||
m_allow_to_use_dice = true;
|
||
m_role_order = {
|
||
battle::Role::Warrior, battle::Role::Pioneer, battle::Role::Medic,
|
||
battle::Role::Tank, battle::Role::Sniper, battle::Role::Caster,
|
||
battle::Role::Support, battle::Role::Special, battle::Role::Drone,
|
||
};
|
||
}
|
||
else {
|
||
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); }) |
|
||
views::transform(&Point::to_string);
|
||
if (!invalid_homes_pos.empty()) {
|
||
Log.error("No replacement homes point:", invalid_homes_pos);
|
||
}
|
||
Log.info("replacement home:", homes_pos | views::transform(&Point::to_string));
|
||
}
|
||
|
||
if (m_homes.empty()) {
|
||
Log.error("Unknown home pos");
|
||
return false;
|
||
}
|
||
m_homes_status.resize(m_homes.size());
|
||
|
||
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);
|
||
|
||
return true;
|
||
}
|
||
|
||
asst::battle::LocationType asst::RoguelikeBattleTaskPlugin::get_oper_location_type(
|
||
const battle::DeploymentOper& oper) const
|
||
{
|
||
return BattleData.get_location_type(oper_name_in_config(oper));
|
||
}
|
||
|
||
asst::battle::OperPosition asst::RoguelikeBattleTaskPlugin::get_role_position(const battle::Role& role) const
|
||
{
|
||
switch (role) {
|
||
case battle::Role::Support:
|
||
case battle::Role::Sniper:
|
||
case battle::Role::Caster:
|
||
return battle::OperPosition::AirDefense;
|
||
break;
|
||
case battle::Role::Pioneer:
|
||
case battle::Role::Warrior:
|
||
case battle::Role::Tank:
|
||
return battle::OperPosition::Blocking;
|
||
break;
|
||
case battle::Role::Medic:
|
||
return m_force_air_defense.ban_medic ? battle::OperPosition::None : battle::OperPosition::AirDefense;
|
||
break;
|
||
case battle::Role::Special:
|
||
case battle::Role::Drone:
|
||
default:
|
||
return battle::OperPosition::None;
|
||
break;
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::set_position_full(battle::LocationType loc_type, bool full)
|
||
{
|
||
switch (loc_type) {
|
||
case battle::LocationType::Melee:
|
||
m_melee_full = full;
|
||
break;
|
||
case battle::LocationType::Ranged:
|
||
m_ranged_full = full;
|
||
break;
|
||
case battle::LocationType::All:
|
||
m_melee_full = full;
|
||
m_ranged_full = full;
|
||
break;
|
||
case battle::LocationType::Invalid:
|
||
case battle::LocationType::None:
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::set_position_full(const Point& loc, bool full)
|
||
{
|
||
if (auto tile_iter = m_normal_tile_info.find(loc); tile_iter != m_normal_tile_info.end()) {
|
||
set_position_full(tile_iter->second.buildable, full);
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::set_position_full(const battle::DeploymentOper& oper, bool full)
|
||
{
|
||
set_position_full(get_oper_location_type(oper), full);
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::get_position_full(battle::LocationType loc_type) const
|
||
{
|
||
switch (loc_type) {
|
||
case battle::LocationType::Melee:
|
||
return m_melee_full;
|
||
break;
|
||
case battle::LocationType::Ranged:
|
||
return m_ranged_full;
|
||
break;
|
||
case battle::LocationType::All:
|
||
return m_melee_full && m_ranged_full;
|
||
break;
|
||
case battle::LocationType::Invalid:
|
||
case battle::LocationType::None:
|
||
default:
|
||
break;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::get_position_full(const battle::DeploymentOper& oper) const
|
||
{
|
||
return get_position_full(get_oper_location_type(oper));
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::do_best_deploy()
|
||
{
|
||
LogTraceFunction;
|
||
Log.info("m_kills", m_kills);
|
||
|
||
bool is_success = false;
|
||
for (const auto& info : m_retreat_plan) {
|
||
if (m_kills >= info.kill_lower_bound && m_kills <= info.kill_upper_bound) {
|
||
if (m_used_tiles.contains(info.location)) {
|
||
retreat_oper(info.location);
|
||
Log.info("retreat operator");
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
Log.info("No operator needs to be retreated.");
|
||
// 获取肉鸽主题
|
||
std::string rogue_theme = status()->get_properties(Status::RoguelikeTheme).value();
|
||
// 构造当前地图的部署指令列表
|
||
std::vector<DeployPlanInfo> deploy_plan_list;
|
||
// 获取当前肉鸽的分组信息[干员组1名称,干员组2名称,...]
|
||
const auto& groups = RoguelikeRecruit.get_group_info(rogue_theme);
|
||
// 获取当前肉鸽的分组内排名信息
|
||
// const auto& group_rank = RoguelikeRecruit.get_group_rank(rogue_theme);
|
||
for (const auto& [name, oper] : m_cur_deployment_opers) {
|
||
// 干员冷却中
|
||
if (oper.cooling) {
|
||
Log.info("operator", oper.name, "is cooling now.");
|
||
continue;
|
||
}
|
||
|
||
const std::string& oper_name = oper_name_in_config(oper);
|
||
// 获取招募信息
|
||
const auto& recruit_info = RoguelikeRecruit.get_oper_info(rogue_theme, oper_name);
|
||
// 获取会用到该干员的干员组[干员组1序号,干员组2序号,...]
|
||
std::vector<int> group_ids = RoguelikeRecruit.get_group_id(rogue_theme, oper_name);
|
||
|
||
for (const auto& group_id : group_ids) {
|
||
// 当前干员组名,string类型
|
||
std::string group_name = groups[group_id];
|
||
Log.trace(m_stage_name, "group_name", group_name);
|
||
if (m_deploy_plan.contains(group_name)) {
|
||
for (const auto& info : m_deploy_plan[group_name]) {
|
||
if (m_kills < info.kill_lower_bound || m_kills > info.kill_upper_bound) {
|
||
Log.trace(" deploy info", oper.name, "in group", group_name, "is waiting.");
|
||
is_success = true; // 如果发现了待命干员,此函数最终返回true
|
||
continue;
|
||
}
|
||
|
||
DeployPlanInfo deploy_plan;
|
||
deploy_plan.oper_name = oper.name;
|
||
// deploy_plan.oper_priority = recruit_info.recruit_priority;
|
||
// deploy_plan.oper_order_in_group = recruit_info.order_in_group.at(group_id);
|
||
|
||
deploy_plan.oper_order_in_group =
|
||
recruit_info.order_in_group.empty() ? INT_MAX : recruit_info.order_in_group.at(group_id);
|
||
deploy_plan.rank = info.rank;
|
||
deploy_plan.placed = info.location;
|
||
deploy_plan.direction = info.direction;
|
||
deploy_plan_list.emplace_back(deploy_plan);
|
||
Log.trace(" deploy info", deploy_plan.oper_name, "is No. ", deploy_plan.oper_order_in_group + 1,
|
||
"in group", group_name, ", with deploy command rank", deploy_plan.rank);
|
||
}
|
||
}
|
||
else {
|
||
Log.error(m_stage_name, "operator", oper.name, "is not in the deploy plan.");
|
||
}
|
||
}
|
||
}
|
||
std::sort(deploy_plan_list.begin(), deploy_plan_list.end());
|
||
for (const auto& deploy_plan : deploy_plan_list) {
|
||
if (!m_used_tiles.contains(deploy_plan.placed) &&
|
||
!m_blacklist_location.contains(deploy_plan.placed)) { // 判断该位置是否已被占据
|
||
auto& oper = m_cur_deployment_opers[deploy_plan.oper_name];
|
||
if (!oper.available) { // 等费
|
||
Log.trace(" best deploy is", deploy_plan.oper_name, "with rank", deploy_plan.rank,
|
||
"but now waiting for cost.");
|
||
return true;
|
||
}
|
||
deploy_oper(deploy_plan.oper_name, deploy_plan.placed, deploy_plan.direction);
|
||
const auto& oper_info = RoguelikeRecruit.get_oper_info(rogue_theme, deploy_plan.oper_name);
|
||
m_skill_usage[deploy_plan.oper_name] = oper_info.skill_usage;
|
||
m_skill_times[deploy_plan.oper_name] = oper_info.skill_times;
|
||
Log.trace(" best deploy is", deploy_plan.oper_name, "with rank", deploy_plan.rank);
|
||
return true;
|
||
}
|
||
}
|
||
return is_success;
|
||
}
|
||
|
||
bool asst::RoguelikeBattleTaskPlugin::do_once()
|
||
{
|
||
check_drone_tiles();
|
||
|
||
cv::Mat image = ctrler()->get_image();
|
||
if (!m_first_deploy && use_all_ready_skill(image)) {
|
||
return true;
|
||
}
|
||
|
||
std::unordered_set<std::string> pre_cooling;
|
||
for (const auto& [name, oper] : m_cur_deployment_opers) {
|
||
if (oper.cooling) pre_cooling.emplace(name);
|
||
}
|
||
auto pre_battlefield = m_battlefield_opers;
|
||
|
||
if (!update_deployment(false, image)) {
|
||
return false;
|
||
}
|
||
update_cost();
|
||
update_kills();
|
||
|
||
std::unordered_set<std::string> cur_cooling;
|
||
size_t cur_available_count = 0; // without drones
|
||
size_t cur_deployments_count = 0; // without drones
|
||
for (const auto& [name, oper] : m_cur_deployment_opers) {
|
||
if (oper.role == Role::Drone) continue;
|
||
|
||
++cur_deployments_count;
|
||
if (oper.cooling) cur_cooling.emplace(name);
|
||
if (oper.available) ++cur_available_count;
|
||
}
|
||
|
||
if (do_best_deploy()) { // 这是新的部署逻辑,更加精确
|
||
m_first_deploy = false;
|
||
return true;
|
||
}
|
||
else { // 这是旧的、通用部署逻辑
|
||
auto urgent_home_opt = check_urgent(pre_cooling, cur_cooling, pre_battlefield);
|
||
|
||
battle::DeploymentOper best_oper;
|
||
bool best_oper_is_dice = false;
|
||
size_t normal_home_index = m_cur_home_index;
|
||
if (!urgent_home_opt) {
|
||
// 不要随便谁好了就上,等等费用,下点厉害的干员
|
||
// TODO: 这个逻辑目前太简单了,待优化
|
||
bool not_battlefield_too_few = m_battlefield_opers.size() > m_homes.size();
|
||
bool available_too_few = cur_available_count <= cur_deployments_count / 2;
|
||
bool not_too_many_cooling = cur_cooling.size() < cur_available_count;
|
||
|
||
if (not_battlefield_too_few && available_too_few && not_too_many_cooling) {
|
||
Log.info("wait a minute");
|
||
return true;
|
||
}
|
||
}
|
||
else {
|
||
m_cur_home_index = *urgent_home_opt;
|
||
|
||
if (m_allow_to_use_dice) {
|
||
auto deployment_key_views = m_cur_deployment_opers | views::keys;
|
||
auto dice_key_iter = ranges::find_first_of(deployment_key_views, DiceSet);
|
||
if (dice_key_iter != deployment_key_views.end()) {
|
||
best_oper_is_dice = true;
|
||
best_oper = m_cur_deployment_opers[*dice_key_iter];
|
||
}
|
||
}
|
||
}
|
||
|
||
Log.info("To path", m_cur_home_index);
|
||
|
||
if (!best_oper_is_dice) {
|
||
if (auto best_oper_opt = calc_best_oper()) {
|
||
best_oper = *best_oper_opt;
|
||
}
|
||
else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 计算最优部署位置及方向
|
||
auto best_loc_opt = calc_best_loc(best_oper);
|
||
if (!best_loc_opt) {
|
||
Log.info("Tiles full while calc best plan.");
|
||
set_position_full(best_oper, true);
|
||
return true;
|
||
}
|
||
const auto& [placed_loc, direction] = *best_loc_opt;
|
||
|
||
deploy_oper(best_oper.name, placed_loc, direction);
|
||
postproc_of_deployment_conditions(best_oper, placed_loc, direction);
|
||
|
||
m_first_deploy = false;
|
||
|
||
const auto& oper_info = RoguelikeRecruit.get_oper_info(
|
||
status()->get_properties(Status::RoguelikeTheme).value(), best_oper.name);
|
||
m_skill_usage[best_oper.name] = oper_info.skill_usage;
|
||
m_skill_times[best_oper.name] = oper_info.skill_times;
|
||
|
||
if (urgent_home_opt) {
|
||
m_urgent_home_index.pop_front();
|
||
m_cur_home_index = normal_home_index;
|
||
}
|
||
++m_cur_home_index;
|
||
if (m_cur_home_index >= m_homes.size()) {
|
||
m_cur_home_index = 0;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::postproc_of_deployment_conditions(const battle::DeploymentOper& oper,
|
||
const Point& placed_loc,
|
||
battle::DeployDirection direction)
|
||
{
|
||
// 如果是医疗干员,判断覆盖范围内有无第一次放置的干员
|
||
if (oper.role == battle::Role::Medic) {
|
||
std::vector<size_t> contain_index;
|
||
for (const Point& relative_pos : get_attack_range(oper, direction)) {
|
||
Point absolute_pos = placed_loc + relative_pos;
|
||
if (auto iter = m_blocking_for_home_index.find(absolute_pos); iter != m_blocking_for_home_index.end()) {
|
||
m_homes_status[iter->second].wait_medic = false;
|
||
contain_index.emplace_back(iter->second);
|
||
}
|
||
}
|
||
if (!contain_index.empty()) {
|
||
m_medic_for_home_index.emplace(placed_loc, std::move(contain_index));
|
||
}
|
||
|
||
m_homes_status[m_cur_home_index].wait_medic = false;
|
||
}
|
||
|
||
auto position = get_role_position(oper.role);
|
||
switch (position) {
|
||
case OperPosition::Blocking: {
|
||
m_force_air_defense.has_deployed_blocking_num++;
|
||
bool& wait_blocking = m_homes_status[m_cur_home_index].wait_blocking;
|
||
if (wait_blocking) {
|
||
wait_blocking = false;
|
||
m_blocking_for_home_index.emplace(placed_loc, m_cur_home_index);
|
||
}
|
||
} break;
|
||
case OperPosition::AirDefense:
|
||
if (!m_force_air_defense.has_finished_deploy_air_defense) {
|
||
m_force_air_defense.has_deployed_air_defense_num++;
|
||
if (m_force_air_defense.has_deployed_air_defense_num >= m_force_air_defense.deploy_air_defense_num) {
|
||
m_force_air_defense.has_finished_deploy_air_defense = true;
|
||
Log.info("FORCE RANGED OPER DEPLOY END");
|
||
}
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (m_force_air_defense.has_finished_deploy_air_defense && position != OperPosition::AirDefense) {
|
||
Log.info("FORCE RANGED OPER DEPLOY END");
|
||
m_force_air_defense.has_finished_deploy_air_defense = true;
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::check_drone_tiles()
|
||
{
|
||
// 将存在于场上超过限时的召唤物所在的地块重新设为可用
|
||
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()) {
|
||
Log.info("Drone at location (", placed_loc.x, ",", placed_loc.y, ") is recognized as retreated");
|
||
set_position_full(placed_loc, false);
|
||
m_battlefield_opers.erase(iter->second);
|
||
m_used_tiles.erase(iter);
|
||
}
|
||
m_need_clear_tiles.pop();
|
||
}
|
||
}
|
||
|
||
std::optional<size_t> asst::RoguelikeBattleTaskPlugin::check_urgent(const std::unordered_set<std::string>& pre_cooling,
|
||
const std::unordered_set<std::string>& cur_cooling,
|
||
const std::map<std::string, Point>& pre_battlefield)
|
||
{
|
||
std::vector<size_t> new_urgent;
|
||
for (const auto& name : cur_cooling) {
|
||
if (pre_cooling.find(name) != pre_cooling.cend()) {
|
||
continue;
|
||
}
|
||
Log.info(name, "retreated");
|
||
auto pre_loc_iter = pre_battlefield.find(name);
|
||
if (pre_loc_iter == pre_battlefield.cend()) {
|
||
Log.error("the oper", name, "was not on the battlefield before");
|
||
continue;
|
||
}
|
||
Point pre_loc = pre_loc_iter->second;
|
||
|
||
if (auto del_loc_blocking = m_blocking_for_home_index.find(pre_loc);
|
||
del_loc_blocking != m_blocking_for_home_index.end()) {
|
||
Log.info("Urgent situation detected");
|
||
|
||
size_t home_index = del_loc_blocking->second;
|
||
m_homes_status[home_index].wait_blocking = true;
|
||
new_urgent.emplace_back(home_index);
|
||
m_blocking_for_home_index.erase(del_loc_blocking);
|
||
}
|
||
else if (auto del_loc_medic = m_medic_for_home_index.find(pre_loc);
|
||
del_loc_medic != m_medic_for_home_index.end()) {
|
||
for (const size_t& home_index : del_loc_medic->second) {
|
||
m_homes_status[home_index].wait_medic = true;
|
||
}
|
||
m_medic_for_home_index.erase(del_loc_medic);
|
||
}
|
||
set_position_full(pre_loc, false);
|
||
}
|
||
|
||
// 同时出现了多个家门的紧急情况,index 序号小的是靠前的家门,优先入栈
|
||
ranges::sort(new_urgent);
|
||
for (const size_t& home_index : new_urgent) {
|
||
if (ranges::find(m_urgent_home_index, home_index) != m_urgent_home_index.cend()) {
|
||
continue;
|
||
}
|
||
m_urgent_home_index.emplace_back(home_index);
|
||
}
|
||
|
||
if (m_urgent_home_index.empty()) {
|
||
return std::nullopt;
|
||
}
|
||
return m_urgent_home_index.front();
|
||
}
|
||
|
||
std::optional<asst::battle::DeploymentOper> asst::RoguelikeBattleTaskPlugin::calc_best_oper() const
|
||
{
|
||
bool has_medic = false;
|
||
bool has_blocking = false;
|
||
bool has_air_defense = false;
|
||
for (const auto& oper : m_cur_deployment_opers | views::values) {
|
||
const auto& role = oper.role;
|
||
switch (role) {
|
||
case Role::Medic:
|
||
has_medic = true;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
const auto& position = get_role_position(role);
|
||
switch (position) {
|
||
case OperPosition::Blocking:
|
||
has_blocking = true;
|
||
break;
|
||
case OperPosition::AirDefense:
|
||
has_air_defense = true;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// found all
|
||
if (has_medic && has_blocking && has_air_defense) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
bool use_air_defense =
|
||
has_air_defense && !m_force_air_defense.has_finished_deploy_air_defense &&
|
||
m_force_air_defense.has_deployed_blocking_num >= m_force_air_defense.stop_blocking_deploy_num && !m_ranged_full;
|
||
bool use_blocking = has_blocking && m_homes_status[m_cur_home_index].wait_blocking && !m_melee_full;
|
||
bool use_medic = has_medic && m_homes_status[m_cur_home_index].wait_medic && !m_ranged_full;
|
||
Log.trace("use_air_defense", use_air_defense, ", use_blocking", use_blocking, ", use_medic", use_medic);
|
||
|
||
std::vector<DeploymentOper> cur_available;
|
||
for (const auto& oper : m_cur_deployment_opers | views::values) {
|
||
if (oper.available) cur_available.emplace_back(oper);
|
||
}
|
||
// 费用高的优先用,放前面
|
||
ranges::sort(cur_available, [&](const auto& lhs, const auto& rhs) {
|
||
return m_cur_deployment_opers.at(lhs.name).cost > m_cur_deployment_opers.at(rhs.name).cost;
|
||
});
|
||
|
||
DeploymentOper best_oper;
|
||
|
||
for (auto role : m_role_order) {
|
||
battle::OperPosition position = get_role_position(role);
|
||
if (use_air_defense) {
|
||
if (position != battle::OperPosition::AirDefense) {
|
||
continue;
|
||
}
|
||
}
|
||
else if (use_blocking) {
|
||
if (position != battle::OperPosition::Blocking) {
|
||
continue;
|
||
}
|
||
}
|
||
else if (use_medic) {
|
||
if (role != battle::Role::Medic) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
for (const battle::DeploymentOper& oper : cur_available) {
|
||
if (oper.role == role && !DiceSet.contains(oper.name) && !get_position_full(oper)) {
|
||
best_oper = oper;
|
||
break;
|
||
}
|
||
}
|
||
if (!best_oper.name.empty()) {
|
||
break;
|
||
}
|
||
}
|
||
if (best_oper.name.empty()) {
|
||
Log.info("No best oper");
|
||
return std::nullopt;
|
||
}
|
||
Log.info("best oper is", best_oper.name);
|
||
return best_oper;
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::all_melee_retreat()
|
||
{
|
||
std::vector<Point> retreat_locs {};
|
||
for (const auto& loc : m_used_tiles | views::keys) {
|
||
auto& tile_info = m_normal_tile_info[loc];
|
||
auto& type = tile_info.buildable;
|
||
if (type == battle::LocationType::Melee || type == battle::LocationType::All) {
|
||
retreat_locs.push_back(loc);
|
||
}
|
||
}
|
||
for (const auto& loc : retreat_locs) {
|
||
retreat_oper(loc);
|
||
}
|
||
}
|
||
|
||
void asst::RoguelikeBattleTaskPlugin::clear()
|
||
{
|
||
BattleHelper::clear();
|
||
m_stage_name.clear();
|
||
|
||
m_homes.clear();
|
||
m_allow_to_use_dice = true;
|
||
m_blacklist_location.clear();
|
||
m_force_deploy_direction.clear();
|
||
m_force_air_defense = decltype(m_force_air_defense)();
|
||
|
||
m_cur_home_index = 0;
|
||
m_first_deploy = true;
|
||
m_melee_full = false;
|
||
m_ranged_full = false;
|
||
m_homes_status.clear();
|
||
m_blocking_for_home_index.clear();
|
||
m_medic_for_home_index.clear();
|
||
m_urgent_home_index = decltype(m_urgent_home_index)();
|
||
m_need_clear_tiles = decltype(m_need_clear_tiles)();
|
||
m_deploy_plan.clear();
|
||
m_retreat_plan.clear();
|
||
}
|
||
|
||
std::vector<asst::Point> asst::RoguelikeBattleTaskPlugin::available_locations(const DeploymentOper& oper) const
|
||
{
|
||
auto type = get_oper_location_type(oper);
|
||
if (type == LocationType::Invalid || type == LocationType::None) {
|
||
return available_locations(get_role_usual_location(oper.role));
|
||
}
|
||
return available_locations(type);
|
||
}
|
||
|
||
std::vector<asst::Point> asst::RoguelikeBattleTaskPlugin::available_locations(battle::LocationType type) const
|
||
{
|
||
std::vector<Point> result;
|
||
for (const auto& [loc, tile] : m_normal_tile_info) {
|
||
bool position_mathced = tile.buildable == type || tile.buildable == battle::LocationType::All;
|
||
position_mathced |= (type == battle::LocationType::All) && (tile.buildable == battle::LocationType::Melee ||
|
||
tile.buildable == battle::LocationType::Ranged);
|
||
if (position_mathced &&
|
||
tile.key != TilePack::TileKey::DeepSea && // 水上要先放板子才能放人,肉鸽里也没板子,那就当作不可放置
|
||
!m_used_tiles.contains(loc) && !m_blacklist_location.contains(loc)) {
|
||
result.emplace_back(loc);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
asst::battle::AttackRange asst::RoguelikeBattleTaskPlugin::get_attack_range(const battle::DeploymentOper& oper,
|
||
DeployDirection direction) const
|
||
{
|
||
int64_t elite = status()->get_number(Status::RoguelikeCharElitePrefix + oper.name).value_or(0);
|
||
battle::AttackRange right_attack_range = BattleData.get_range(oper_name_in_config(oper), elite);
|
||
|
||
if (right_attack_range == BattleDataConfig::EmptyRange) {
|
||
switch (oper.role) {
|
||
case battle::Role::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 battle::Role::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 battle::Role::Medic:
|
||
case battle::Role::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 battle::Role::Warrior:
|
||
right_attack_range = { Point(0, 0), Point(1, 0), Point(2, 0) };
|
||
break;
|
||
case battle::Role::Special:
|
||
case battle::Role::Tank:
|
||
case battle::Role::Pioneer:
|
||
case battle::Role::Drone:
|
||
right_attack_range = { Point(0, 0), Point(1, 0) };
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (auto cur_direction :
|
||
{ DeployDirection::Right, DeployDirection::Up, DeployDirection::Left, DeployDirection::Down }) {
|
||
if (cur_direction == direction) break;
|
||
|
||
// rotate relative attack range counterclockwise
|
||
for (Point& point : right_attack_range)
|
||
point = { point.y, -point.x };
|
||
}
|
||
|
||
return right_attack_range;
|
||
}
|
||
|
||
std::optional<asst::RoguelikeBattleTaskPlugin::DeployInfo> asst::RoguelikeBattleTaskPlugin::calc_best_loc(
|
||
const battle::DeploymentOper& oper) const
|
||
{
|
||
size_t home_index = m_cur_home_index;
|
||
if (home_index >= m_homes.size()) {
|
||
Log.warn("home index is out of range", m_cur_home_index, m_homes.size());
|
||
home_index = 0;
|
||
}
|
||
const ReplacementHome& home = m_homes[home_index];
|
||
|
||
auto comp_dist = [&](const Point& lhs, const Point& rhs) -> bool {
|
||
int lhs_y_dist = std::abs(lhs.y - home.location.y);
|
||
int lhs_dist = std::abs(lhs.x - home.location.x) + lhs_y_dist;
|
||
int rhs_y_dist = std::abs(rhs.y - home.location.y);
|
||
int rhs_dist = std::abs(rhs.x - home.location.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);
|
||
if (available_loc.empty()) {
|
||
Log.error("No available locations");
|
||
return std::nullopt;
|
||
}
|
||
// 把所有可用的点按距离排个序
|
||
ranges::sort(available_loc, comp_dist);
|
||
|
||
if (DiceSet.contains(oper.name)) {
|
||
return DeployInfo { available_loc.back(), DeployDirection::None };
|
||
}
|
||
|
||
Point best_location;
|
||
DeployDirection best_direction = DeployDirection::None;
|
||
int max_score = INT_MIN;
|
||
|
||
const auto& near_loc = available_loc.front();
|
||
int min_dist = std::abs(near_loc.x - home.location.x) + std::abs(near_loc.y - home.location.y);
|
||
|
||
// 取距离最近的N个点,计算分数。然后使用得分最高的点
|
||
constexpr int CalcPointCount = 4;
|
||
for (const auto& loc : available_loc | views::take(CalcPointCount)) {
|
||
const auto& [cur_direction, cur_socre] = calc_best_direction_and_score(loc, oper, home.direction);
|
||
// 离得远的要扣分
|
||
constexpr int DistWeights = -1050;
|
||
int extra_dist = std::abs(loc.x - home.location.x) + std::abs(loc.y - home.location.y) - min_dist;
|
||
int extra_dist_score = DistWeights * extra_dist;
|
||
if (oper.role == battle::Role::Medic) { // 医疗干员离得远无所谓
|
||
extra_dist_score = 0;
|
||
}
|
||
|
||
if (cur_socre + extra_dist_score > max_score) {
|
||
max_score = cur_socre + extra_dist_score;
|
||
best_location = loc;
|
||
best_direction = cur_direction;
|
||
}
|
||
}
|
||
|
||
// 强制变化为确定的攻击方向
|
||
if (auto iter = m_force_deploy_direction.find(best_location); iter != m_force_deploy_direction.end()) {
|
||
if (iter->second.role.contains(oper.role)) {
|
||
best_direction = iter->second.direction;
|
||
}
|
||
}
|
||
|
||
return DeployInfo { best_location, best_direction };
|
||
}
|
||
|
||
asst::RoguelikeBattleTaskPlugin::DirectionAndScore asst::RoguelikeBattleTaskPlugin::calc_best_direction_and_score(
|
||
Point loc, const battle::DeploymentOper& oper, DeployDirection recommended_direction) const
|
||
{
|
||
LogTraceFunction;
|
||
|
||
size_t home_index = m_cur_home_index;
|
||
if (home_index >= m_homes.size()) {
|
||
Log.warn("home index is out of range", m_cur_home_index, m_homes.size());
|
||
home_index = 0;
|
||
}
|
||
Point home_loc = m_homes[home_index].location;
|
||
|
||
DeployDirection base_direction = DeployDirection::None;
|
||
if (loc.x == home_loc.x) {
|
||
if (loc.y - home_loc.y >= 0) {
|
||
base_direction = DeployDirection::Up;
|
||
}
|
||
else {
|
||
base_direction = DeployDirection::Down;
|
||
}
|
||
}
|
||
else {
|
||
if (loc.x - home_loc.x >= 0) {
|
||
base_direction = DeployDirection::Right;
|
||
}
|
||
else {
|
||
base_direction = DeployDirection::Left;
|
||
}
|
||
}
|
||
|
||
// 家门反过来
|
||
DeployDirection home_direction = DeployDirection::None;
|
||
switch (base_direction) {
|
||
case DeployDirection::Right:
|
||
home_direction = DeployDirection::Left;
|
||
break;
|
||
case DeployDirection::Up:
|
||
home_direction = DeployDirection::Down;
|
||
break;
|
||
case DeployDirection::Left:
|
||
home_direction = DeployDirection::Right;
|
||
break;
|
||
case DeployDirection::Down:
|
||
home_direction = DeployDirection::Up;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// 医疗优先朝向家门
|
||
if (oper.role == battle::Role::Medic) {
|
||
base_direction = home_direction;
|
||
}
|
||
|
||
int max_score = 0;
|
||
DeployDirection best_direction = DeployDirection::None;
|
||
|
||
for (auto direction :
|
||
{ DeployDirection::Right, DeployDirection::Up, DeployDirection::Left, DeployDirection::Down }) {
|
||
int score = 0;
|
||
// 按朝右算,后面根据方向做转换
|
||
for (const Point& relative_pos : get_attack_range(oper, direction)) {
|
||
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 battle::Role::Medic:
|
||
if (auto iter = m_used_tiles.find(absolute_pos);
|
||
iter != m_used_tiles.cend() &&
|
||
BattleData.get_role(iter->second) != battle::Role::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 != battle::Role::Medic && direction == home_direction) {
|
||
score -= 500;
|
||
}
|
||
if (oper.role != battle::Role::Medic && direction == recommended_direction) {
|
||
score += 2000;
|
||
}
|
||
if (score > max_score) {
|
||
max_score = score;
|
||
best_direction = direction;
|
||
}
|
||
}
|
||
|
||
return { best_direction, max_score };
|
||
}
|