diff --git a/resource/tasks.json b/resource/tasks.json index 3e480837f7..7bf16c3e6a 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -3596,6 +3596,167 @@ ["^资深.+", "资深干员"] ] }, + "UseSupportUnit-Enter": { + "doc": "通过点击编队界面右上角 <助战单位> 文字左边的 Icon 进入助战列表", + "roi": [1023, 314, 46, 50], + "action": "ClickSelf", + "next": ["UseSupportUnit-Enter@NextAfterLoadingText", "UseSupportUnit-InSupportList"] + }, + "UseSupportUnit-InSupportList": { + "doc": "通过识别右上角 <更新助战列表> 文字左边的 Icon 或者 冷却倒计时左边的 Icon 判断是否已进入助战列表", + "template": ["UseSupportUnit-Refresh", "UseSupportUnit-RefreshCooldown"], + "roi": [1096, 12, 61, 52] + }, + "UseSupportUnit-RefreshSupportList": { + "doc": "更新助战列表,如果识别到冷却倒计时则等待倒计时结束后再点击 <更新助战列表> 按钮", + "algorithm": "JustReturn", + "next": ["UseSupportUnit-Refresh", "UseSupportUnit-RefreshCooldown"] + }, + "UseSupportUnit-Refresh": { + "roi": [1096, 12, 61, 52], + "preDelay": 500, + "action": "ClickSelf", + "next": ["UseSupportUnit-Refresh@NextAfterLoadingText", "Stop"] + }, + "UseSupportUnit-RefreshCooldown": { + "roi": [1096, 12, 61, 52], + "postDelay": 1000, + "next": ["UseSupportUnit-RefreshSupportList"] + }, + "UseSupportUnit-LeaveSupportList": { + "doc": "如果已经进入助战列表则点击左上角箭头图标返回编队界面", + "algorithm": "JustReturn", + "next": ["UseSupportUnit-LeaveSupportList-InSupportList", "Stop"] + }, + "UseSupportUnit-LeaveSupportList-InSupportList": { + "baseTask": "UseSupportUnit-InSupportList", + "template": ["UseSupportUnit-Refresh", "UseSupportUnit-RefreshCooldown"], + "next": ["UseSupportUnit-ReturnButton"] + }, + "UseSupportUnit-ReturnButton": { + "baseTask": "ReturnButton", + "template": ["Return.png"], + "next": ["UseSupportUnit-LeaveSupportList#next"] + }, + "UseSupportUnit-Role": { + "doc": "模版任务: 招募助战干员时选择职业,若当前职业正确则不进行额外操作", + "algorithm": "JustReturn", + "next": ["UseSupportUnit-RoleSelected", "UseSupportUnit-SelectRole"] + }, + "UseSupportUnit-RoleSelected": { + "doc": "模版任务: 招募助战干员时通过深色背景的职业图标判断当前职业是否正确,Ccoeff 应该够用,若出问题可以考虑换为 RGBCount", + "template": "empty.png", + "roi": [20, 93, 73, 608] + }, + "UseSupportUnit-SelectRole": { + "doc": "模版任务: 招募助战干员时通过点击浅色背景的职业图标切换职业", + "template": "empty.png", + "roi": [20, 93, 73, 608], + "action": "ClickSelf", + "next": ["UseSupportUnit-Role@NextAfterLoadingText"] + }, + "Pioneer@UseSupportUnit-RoleSelected": {}, + "Pioneer@UseSupportUnit-SelectRole": {}, + "Warrior@UseSupportUnit-RoleSelected": {}, + "Warrior@UseSupportUnit-SelectRole": {}, + "Tank@UseSupportUnit-RoleSelected": {}, + "Tank@UseSupportUnit-SelectRole": {}, + "Sniper@UseSupportUnit-RoleSelected": {}, + "Sniper@UseSupportUnit-SelectRole": {}, + "Caster@UseSupportUnit-RoleSelected": {}, + "Caster@UseSupportUnit-SelectRole": {}, + "Medic@UseSupportUnit-RoleSelected": {}, + "Medic@UseSupportUnit-SelectRole": {}, + "Support@UseSupportUnit-RoleSelected": {}, + "Support@UseSupportUnit-SelectRole": {}, + "Special@UseSupportUnit-RoleSelected": {}, + "Special@UseSupportUnit-SelectRole": {}, + "UseSupportUnit-SwipeToTheLeft": { + "baseTask": "SwipeToTheLeft", + "postDelay": 1000 + }, + "UseSupportUnit-SwipeToTheRight": { + "baseTask": "SwipeToTheRight", + "postDelay": 1000 + }, + "UseSupportUnit-MaxSpecLvl-1": { + "template": "UseSupportUnit-MaxSpecLvl.png", + "roi": [736, 251, 24, 22] + }, + "UseSupportUnit-MaxSpecLvl-2": { + "template": "UseSupportUnit-MaxSpecLvl.png", + "roi": [851, 251, 24, 22] + }, + "UseSupportUnit-MaxSpecLvl-3": { + "template": "UseSupportUnit-MaxSpecLvl.png", + "roi": [966, 251, 24, 22] + }, + "UseSupportUnit-SelectSkill-1": { + "template": "UseSupportUnit-SelectSkill.png", + "action": "ClickSelf", + "roi": [781, 320, 66, 38] + }, + "UseSupportUnit-SelectSkill-2": { + "template": "UseSupportUnit-SelectSkill.png", + "action": "ClickSelf", + "roi": [900, 320, 64, 38] + }, + "UseSupportUnit-SelectSkill-3": { + "template": "UseSupportUnit-SelectSkill.png", + "action": "ClickSelf", + "roi": [1010, 320, 70, 38] + }, + "UseSupportUnit-Confirm": { + "doc": "识别 <招募助战> 文字并点击", + "baseTask": "RecruitSupportConfirm" + }, + "UseSupportUnit-Cancel": { + "doc": "若识别到 <招募助战> 文字就点击屏幕下方取消选择", + "baseTask": "RecruitSupportConfirm", + "action": "ClickRect", + "specificRect": [640, 700, 0, 0], + "postDelay": 500 + }, + "SupportListAnalyzer-Flag": { + "doc": "识别每个助战干员栏位底部的 Icon,后续根据相对位置识别其他信息", + "template": ["SupportListAnalyzer-Friend.png", "SupportListAnalyzer-NonFriend.png"], + "roi": [95, 570, 1185, 90], + "method": "RGBCount", + "colorScales": [ + [ + [150, 0, 0], + [255, 255, 255] + ] + ] + }, + "SupportListAnalyzer-NameOcr": { + "doc": "若助战干员栏位底部 Icon 的 Rect 为 r, 则干员名识别用 roi 为 [r.x + roi.x, r.y - roi.y, roi.w, roi.h]", + "baseTask": "CharsNameOcrReplace", + "roi": [0, -37, 129, 37] + }, + "SupportListAnalyzer-Elite": { + "doc": "若助战干员栏位底部 Icon 的 Rect 为 r, 则干员精英化等级识别用 roi 为 [r.x + roi.x, r.y - roi,y, roi.w, roi.h]", + "template": ["SupportListAnalyzer-Elite-2", "SupportListAnalyzer-Elite-1"], + "roi": [35, -187, 60, 50], + "templThreshold": 0.7, + "method": ["RGBCount", "RGBCount"], + "colorScales": [ + [ + [250, 250, 250], + [255, 255, 255] + ] + ] + }, + "SupportListAnalyzer-LevelOcr": { + "doc": "若助战干员栏位底部 Icon 的 Rect 为 r, 则干员等级识别用 roi 为 [r.x + roi.x, r.y - roi,y, roi.w, roi.h]", + "baseTask": "NumberOcrReplace", + "roi": [76, -130, 45, 20] + }, + "SelectSupportConfirmToFormationPage": { + "baseTask": "BattleQuickFormationConfirm", + "template": "BattleQuickFormationConfirm.png", + "next": ["UseSupportUnit-Enter"] + }, "RecruitRefresh": { "action": "ClickSelf", "roi": [850, 340, 220, 170], diff --git a/resource/template/Caster@UseSupportUnit-RoleSelected.png b/resource/template/Caster@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..1a34af9941 Binary files /dev/null and b/resource/template/Caster@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Caster@UseSupportUnit-SelectRole.png b/resource/template/Caster@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..9f046e8798 Binary files /dev/null and b/resource/template/Caster@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/Medic@UseSupportUnit-RoleSelected.png b/resource/template/Medic@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..12dd699d8c Binary files /dev/null and b/resource/template/Medic@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Medic@UseSupportUnit-SelectRole.png b/resource/template/Medic@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..f18b298ff0 Binary files /dev/null and b/resource/template/Medic@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/Pioneer@UseSupportUnit-RoleSelected.png b/resource/template/Pioneer@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..62bdf87650 Binary files /dev/null and b/resource/template/Pioneer@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Pioneer@UseSupportUnit-SelectRole.png b/resource/template/Pioneer@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..5c03b45ecd Binary files /dev/null and b/resource/template/Pioneer@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/Sniper@UseSupportUnit-RoleSelected.png b/resource/template/Sniper@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..2a88813d16 Binary files /dev/null and b/resource/template/Sniper@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Sniper@UseSupportUnit-SelectRole.png b/resource/template/Sniper@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..7299ede5b0 Binary files /dev/null and b/resource/template/Sniper@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/Special@UseSupportUnit-RoleSelected.png b/resource/template/Special@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..d9742b8e58 Binary files /dev/null and b/resource/template/Special@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Special@UseSupportUnit-SelectRole.png b/resource/template/Special@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..642f9fe05b Binary files /dev/null and b/resource/template/Special@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/Support@UseSupportUnit-RoleSelected.png b/resource/template/Support@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..bc2bd52b0f Binary files /dev/null and b/resource/template/Support@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Support@UseSupportUnit-SelectRole.png b/resource/template/Support@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..fab16d047a Binary files /dev/null and b/resource/template/Support@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/SupportListAnalyzer-Elite-1.png b/resource/template/SupportListAnalyzer-Elite-1.png new file mode 100644 index 0000000000..fea3524fbc Binary files /dev/null and b/resource/template/SupportListAnalyzer-Elite-1.png differ diff --git a/resource/template/SupportListAnalyzer-Elite-2.png b/resource/template/SupportListAnalyzer-Elite-2.png new file mode 100644 index 0000000000..57c1c84296 Binary files /dev/null and b/resource/template/SupportListAnalyzer-Elite-2.png differ diff --git a/resource/template/SupportListAnalyzer-Friend.png b/resource/template/SupportListAnalyzer-Friend.png new file mode 100644 index 0000000000..2c1b76b61f Binary files /dev/null and b/resource/template/SupportListAnalyzer-Friend.png differ diff --git a/resource/template/SupportListAnalyzer-NonFriend.png b/resource/template/SupportListAnalyzer-NonFriend.png new file mode 100644 index 0000000000..3d1ba82a90 Binary files /dev/null and b/resource/template/SupportListAnalyzer-NonFriend.png differ diff --git a/resource/template/Tank@UseSupportUnit-RoleSelected.png b/resource/template/Tank@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..7b9c664e92 Binary files /dev/null and b/resource/template/Tank@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Tank@UseSupportUnit-SelectRole.png b/resource/template/Tank@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..1dc6ddb4fe Binary files /dev/null and b/resource/template/Tank@UseSupportUnit-SelectRole.png differ diff --git a/resource/template/UseSupportUnit-Enter.png b/resource/template/UseSupportUnit-Enter.png new file mode 100644 index 0000000000..3594d328b7 Binary files /dev/null and b/resource/template/UseSupportUnit-Enter.png differ diff --git a/resource/template/UseSupportUnit-MaxSpecLvl.png b/resource/template/UseSupportUnit-MaxSpecLvl.png new file mode 100644 index 0000000000..23003ff98f Binary files /dev/null and b/resource/template/UseSupportUnit-MaxSpecLvl.png differ diff --git a/resource/template/UseSupportUnit-Refresh.png b/resource/template/UseSupportUnit-Refresh.png new file mode 100644 index 0000000000..017bbe61fd Binary files /dev/null and b/resource/template/UseSupportUnit-Refresh.png differ diff --git a/resource/template/UseSupportUnit-RefreshCooldown.png b/resource/template/UseSupportUnit-RefreshCooldown.png new file mode 100644 index 0000000000..a44d9cebce Binary files /dev/null and b/resource/template/UseSupportUnit-RefreshCooldown.png differ diff --git a/resource/template/UseSupportUnit-SelectSkill.png b/resource/template/UseSupportUnit-SelectSkill.png new file mode 100644 index 0000000000..b0feb8a3a7 Binary files /dev/null and b/resource/template/UseSupportUnit-SelectSkill.png differ diff --git a/resource/template/Warrior@UseSupportUnit-RoleSelected.png b/resource/template/Warrior@UseSupportUnit-RoleSelected.png new file mode 100644 index 0000000000..84f87f1896 Binary files /dev/null and b/resource/template/Warrior@UseSupportUnit-RoleSelected.png differ diff --git a/resource/template/Warrior@UseSupportUnit-SelectRole.png b/resource/template/Warrior@UseSupportUnit-SelectRole.png new file mode 100644 index 0000000000..14bbace51b Binary files /dev/null and b/resource/template/Warrior@UseSupportUnit-SelectRole.png differ diff --git a/src/MaaCore/Common/AsstBattleDef.h b/src/MaaCore/Common/AsstBattleDef.h index 10492485cb..93fe98146f 100644 --- a/src/MaaCore/Common/AsstBattleDef.h +++ b/src/MaaCore/Common/AsstBattleDef.h @@ -131,6 +131,22 @@ inline static LocationType get_role_usual_location(const Role& role) } } +struct RequiredOper // 编队/招募需求干员 +{ + Role role = Role::Unknown; + std::string name; + int skill = 0; // 技能序号,取值范围 [0, 3],0时使用默认技能 或 上次编队时使用的技能 + + RequiredOper() = default; + + RequiredOper(Role role_, std::string name_, int skill_) : + role(role_), + name(std::move(name_)), + skill(skill_) + { + } +}; + struct DeploymentOper { size_t index = 0; @@ -341,17 +357,21 @@ 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; - static const std::unordered_map RoleToName = { - { Role::Warrior, "近卫" }, { Role::Pioneer, "先锋" }, { Role::Medic, "医疗" }, - { Role::Tank, "重装" }, { Role::Sniper, "狙击" }, { Role::Caster, "术师" }, - { Role::Support, "辅助" }, { Role::Special, "特种" }, { Role::Drone, "无人机" }, + static const std::unordered_map> RoleToName { + { 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.find(role); iter != RoleToName.end()) { - return iter->second; + return en ? iter->second.second : iter->second.first; } + return "Unknown"; } } diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 95c3f965bc..7e3889c2ed 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -114,6 +114,7 @@ + @@ -199,6 +200,7 @@ + @@ -302,6 +304,7 @@ + @@ -384,6 +387,7 @@ + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index a83810b404..04ab9d9fe1 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -441,6 +441,9 @@ Source\Task\Miscellaneous + + Source\Task\Miscellaneous + Source\Task\Miscellaneous @@ -731,6 +734,9 @@ Source\Controller + + Source\Vision\Battle + @@ -958,6 +964,9 @@ Source\Task\Miscellaneous + + Source\Task\Miscellaneous + Source\Task\Miscellaneous @@ -1227,5 +1236,8 @@ Source\Controller + + Source\Vision\Battle + \ No newline at end of file diff --git a/src/MaaCore/Task/Interface/CopilotTask.cpp b/src/MaaCore/Task/Interface/CopilotTask.cpp index c90e5bc942..7227cb4647 100644 --- a/src/MaaCore/Task/Interface/CopilotTask.cpp +++ b/src/MaaCore/Task/Interface/CopilotTask.cpp @@ -64,6 +64,8 @@ bool asst::CopilotTask::set_params(const json::value& params) { LogTraceFunction; + using SupportUnitUsage = BattleFormationTask::SupportUnitUsage; + if (m_running) { return false; } @@ -85,6 +87,8 @@ bool asst::CopilotTask::set_params(const json::value& params) int select_formation = params.get("select_formation", 0); // 选择第几个编队,0为不选择 bool add_trust = params.get("add_trust", false); // 是否自动补信赖 bool add_user_additional = params.contains("user_additional"); // 是否自动补用户自定义干员 + auto support_unit_usage = static_cast( + params.get("support_unit_usage", static_cast(SupportUnitUsage::WhenNeeded))); // 助战干员使用模式 std::string support_unit_name = params.get("support_unit_name", std::string()); if (params.contains("add_user_additional")) { @@ -145,7 +149,8 @@ bool asst::CopilotTask::set_params(const json::value& params) m_formation_task_ptr->set_enable(with_formation && navigate_name.find("TR") == std::string::npos); m_formation_task_ptr->set_select_formation(select_formation); m_formation_task_ptr->set_add_trust(add_trust); - m_formation_task_ptr->set_support_unit_name(std::move(support_unit_name)); + m_formation_task_ptr->set_support_unit_usage(support_unit_usage); + m_formation_task_ptr->set_specific_support_unit(support_unit_name); if (auto opt = params.find("user_additional"); add_user_additional && opt) { std::vector> user_additional; diff --git a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp index 292c825a1c..14c631a4c2 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.cpp @@ -9,10 +9,41 @@ #include "Config/TaskData.h" #include "Controller/Controller.h" #include "Task/ProcessTask.h" +#include "UseSupportUnitTaskPlugin.h" #include "Utils/ImageIo.hpp" #include "Utils/Logger.hpp" #include "Vision/MultiMatcher.h" +asst::BattleFormationTask::BattleFormationTask( + const AsstCallback& callback, + Assistant* inst, + std::string_view task_chain) : + AbstractTask(callback, inst, task_chain), + m_use_support_unit_task_ptr(std::make_shared(callback, inst, task_chain)) +{ +} + +bool asst::BattleFormationTask::set_specific_support_unit(const std::string& name) +{ + LogTraceFunction; + + if (m_support_unit_usage != SupportUnitUsage::Specific) { + Log.error(__FUNCTION__, "| Current support unit usage is not SupportUnitUsage::Specific"); + return false; + } + + const battle::Role role = (m_specific_support_unit.role = BattleData.get_role(name)); + if (role == battle::Role::Unknown) { + // 无法根据干员名称获取其职业 + Log.error(__FUNCTION__, "| Invalid specific support unit"); + return false; + } + m_specific_support_unit.name = name; // 此处可能需要对阿米娅进行特殊处理 + // 之后在 parse_formation 中,如果发现这名助战干员,则将其技能设定为对应的所需技能 + m_specific_support_unit.skill = 0; + return true; +}; + bool asst::BattleFormationTask::_run() { LogTraceFunction; @@ -34,13 +65,40 @@ bool asst::BattleFormationTask::_run() add_formation(role, oper_groups, missing_operators); } - if (!missing_operators.empty()) { - if (missing_operators.size() == 1) { - // TODO: 自动借助战? + // 在有且仅有一个缺失干员组时尝试寻找助战干员补齐编队 + if (use_suppprt_unit_when_needed() && missing_operators.size() == 1 && !m_used_support_unit) { + // 之后再重构数据结构,先凑合用 + std::vector required_opers; + for (const battle::OperUsage& oper : missing_operators.front().second) { + // 如果指定助战干员正好可以补齐编队,则只招募指定助战干员就好了,记得再次确认一下 skill + // 如果编队里正好有【艾雅法拉 - 2】和 【艾雅法拉 - 3】呢? + if (oper.name == m_specific_support_unit.name) { + m_specific_support_unit.skill = oper.skill; + required_opers.clear(); + required_opers.emplace_back(m_specific_support_unit); + break; + } + required_opers.emplace_back(BattleData.get_role(oper.name), oper.name, oper.skill); } - report_missing_operators(missing_operators); + // 先退出去招募助战再回来,好蠢 + confirm_selection(); + Log.info(__FUNCTION__, "| Left quick formation scene"); + if (m_use_support_unit_task_ptr->try_add_support_unit(required_opers, 5, true)) { + m_used_support_unit = true; + missing_operators.clear(); + } + // 再到快速编队页面 + if (!enter_selection_page()) { + save_img(utils::path("debug") / utils::path("other")); + return false; + } + Log.info(__FUNCTION__, "| Returned to quick formation scene"); + } + // 在尝试补齐编队后依然有缺失干员,自动编队失败 + if (!missing_operators.empty()) { + report_missing_operators(missing_operators); return false; } @@ -71,11 +129,11 @@ bool asst::BattleFormationTask::_run() } confirm_selection(); - // 借一个随机助战 - if (m_support_unit_name == "_RANDOM_") { - if (!select_random_support_unit()) { - return false; - } + if (m_support_unit_usage == SupportUnitUsage::Specific && !m_used_support_unit) { // 使用指定助战干员 + m_used_support_unit = m_use_support_unit_task_ptr->try_add_support_unit({ m_specific_support_unit }, 5, true); + } + else if (m_support_unit_usage == SupportUnitUsage::Random && !m_used_support_unit) { // 使用随机助战干员 + m_used_support_unit = m_use_support_unit_task_ptr->try_add_support_unit({}, 5, false); } return true; @@ -427,6 +485,11 @@ bool asst::BattleFormationTask::parse_formation() battle::Role role = BattleData.get_role(opers_vec.front().name); for (const auto& oper : opers_vec) { same_role &= BattleData.get_role(oper.name) == role; + + // (仅一次)如果发现这名助战干员,则将其技能设定为对应的所需技能 + if (oper.name == m_specific_support_unit.name && m_specific_support_unit.skill == 0) { + m_specific_support_unit.skill = oper.skill; + } } // for unknown, will use { "BattleQuickFormationRole-All", "BattleQuickFormationRole-All-OCR" } diff --git a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h index 96a7c869ad..5addb3e8a2 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h +++ b/src/MaaCore/Task/Miscellaneous/BattleFormationTask.h @@ -7,10 +7,12 @@ namespace asst { +class UseSupportUnitTaskPlugin; + class BattleFormationTask : public AbstractTask { public: - using AbstractTask::AbstractTask; + BattleFormationTask(const AsstCallback& callback, Assistant* inst, std::string_view task_chain); virtual ~BattleFormationTask() override = default; enum class Filter @@ -29,8 +31,6 @@ public: void append_additional_formation(AdditionalFormation formation) { m_additional.emplace_back(std::move(formation)); } - void set_support_unit_name(std::string name) { m_support_unit_name = std::move(name); } - // 设置追加自定干员列表 void set_user_additional(std::vector> value) { m_user_additional = std::move(value); } @@ -53,6 +53,31 @@ public: void set_data_resource(DataResource resource) { m_data_resource = resource; } + // ———————————————————————————————— + // 助战干员选择相关 + // ———————————————————————————————— + enum class SupportUnitUsage // 助战干员使用策略 + { + None = 0, // 不使用助战干员 + WhenNeeded = 1, // 如果有且仅有一名缺失干员则尝试寻找助战干员补齐编队, 如果无缺失干员则不使用助战干员 + Specific = 2, // 如果有且仅有一名缺失干员则尝试寻找助战干员补齐编队,如果无缺失干员则使用指定助战干员 + Random = 3 // 如果有且仅有一名缺失干员则尝试寻找助战干员补齐编队,如果无缺失干员则使用随机助战干员 + }; + + void set_support_unit_usage(const SupportUnitUsage& support_unit_usgae) + { + m_support_unit_usage = support_unit_usgae; + } + + // 是否在有且仅有一名缺失干员时尝试寻找助战干员补齐编队 + bool use_suppprt_unit_when_needed() const + { + return m_support_unit_usage == SupportUnitUsage::WhenNeeded || + m_support_unit_usage == SupportUnitUsage::Specific || m_support_unit_usage == SupportUnitUsage::Random; + } + + bool set_specific_support_unit(const std::string& name = ""); // 设置指定助战干员 + protected: using OperGroup = std::pair>; @@ -83,11 +108,19 @@ protected: std::make_shared>(); // 编队中的干员名称-所属组名 bool m_add_trust = false; // 是否需要追加信赖干员 std::vector> m_user_additional; // 追加干员表,从头往后加 - std::string m_support_unit_name; DataResource m_data_resource = DataResource::Copilot; std::vector m_additional; std::string m_last_oper_name; int m_select_formation_index = 0; int m_missing_retry_times = 1; // 识别不到干员的重试次数 + + // ———————————————————————————————— + // 助战干员选择相关 + // ———————————————————————————————— + std::shared_ptr m_use_support_unit_task_ptr = nullptr; + SupportUnitUsage m_support_unit_usage = SupportUnitUsage::None; + bool m_used_support_unit = false; // 是否已经招募助战干员 + // ———————— 以下变量为指定助战干员设置,仅当 m_support_unit_usage == SupportUnitUsage::Specific 时有效 ———————— + battle::RequiredOper m_specific_support_unit; }; } // namespace asst diff --git a/src/MaaCore/Task/Miscellaneous/CreditFightTask.cpp b/src/MaaCore/Task/Miscellaneous/CreditFightTask.cpp index c7d50774d7..faaaf89ecb 100644 --- a/src/MaaCore/Task/Miscellaneous/CreditFightTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/CreditFightTask.cpp @@ -23,7 +23,7 @@ asst::CreditFightTask::CreditFightTask(const AsstCallback& callback, Assistant* json::value copilot_params = json::object { { "filename", utils::path_to_utf8_string(ResDir.get() / "copilot" / "OF-1_credit_fight.json") }, { "formation", true }, - { "support_unit_name", "_RANDOM_" }, + { "support_unit_usage", 3 }, }; m_copilot_task_ptr->set_params(copilot_params); @@ -48,7 +48,7 @@ void asst::CreditFightTask::set_select_formation(int index) json::value copilot_params = json::object { { "filename", utils::path_to_utf8_string(ResDir.get() / "copilot" / "OF-1_credit_fight.json") }, { "formation", true }, - { "support_unit_name", "_RANDOM_" }, + { "support_unit_usage", 3 }, { "select_formation", index }, }; m_copilot_task_ptr->set_params(copilot_params); diff --git a/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.cpp b/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.cpp new file mode 100644 index 0000000000..dcd1c4f32a --- /dev/null +++ b/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.cpp @@ -0,0 +1,196 @@ +#include "UseSupportUnitTaskPlugin.h" + +#include "Config/GeneralConfig.h" +#include "Config/Miscellaneous/BattleDataConfig.h" +#include "Controller/Controller.h" +#include "Task/ProcessTask.h" +#include "Utils/Logger.hpp" +#include "Vision/Matcher.h" + +bool asst::UseSupportUnitTaskPlugin::try_add_support_unit( + const std::vector& required_opers, + const int max_refresh_times, + const bool max_spec_lvl, + const bool allow_non_friend_support_unit) +{ + LogTraceFunction; + + // 通过点击编队界面右上角 <助战单位> 文字左边的 Icon 进入助战干员选择界面 + ProcessTask(*this, { "UseSupportUnit-Enter" }).run(); + + // 随机模式 + if (required_opers.empty()) { + return try_add_support_unit_for_role( + Role::Unknown, + required_opers, + max_refresh_times, + max_spec_lvl, + allow_non_friend_support_unit); + } + + // 非随机模式 + std::vector temp_required_opers; + for (size_t i = 0; i < 3; ++i) { + if (i >= required_opers.size()) { + break; + } + temp_required_opers.emplace_back(required_opers[i]); + if (try_add_support_unit_for_role( + required_opers[i].role, + temp_required_opers, + max_refresh_times, + max_spec_lvl, + allow_non_friend_support_unit)) { + return true; + } + } + + // 未找到符合要求的助战干员,手动退出助战列表 + Log.info(__FUNCTION__, "| Fail to find any qualified support operator"); + ProcessTask(*this, { "UseSupportUnit-LeaveSupportList" }).run(); + return false; +} + +bool asst::UseSupportUnitTaskPlugin::try_add_support_unit_for_role( + const Role role, + const std::vector& required_opers, + const int max_refresh_times, + const bool max_spec_lvl, + const bool allow_non_friend_support_unit) +{ + LogTraceFunction; + + // 从 required_opers 中筛除职业不为 role 的干员 + auto filtered_view = required_opers | views::filter([&](const RequiredOper& required_oper) { + return role == Role::Unknown || required_oper.role == role; + }); + std::vector filtered_required_opers(filtered_view.begin(), filtered_view.end()); + + // 若 role != Role::Unknown 则切换到对应职业的助战干员列表,否则保留当前职业选择 + if (role != battle::Role::Unknown) { + ProcessTask(*this, { enum_to_string(role, true) + "@UseSupportUnit-Role" }).run(); + } + + // 定位到助战列表最左侧第一页 + for (int i = 0; i < MaxNumSupportListPages - 1; ++i) { + ProcessTask(*this, { "UseSupportUnit-SwipeToTheLeft" }).run(); + } + + // 初始化变量 + SupportListAnalyzer analyzer; + std::unordered_set known_oper_names; + std::vector> candidates(filtered_required_opers.size(), std::nullopt); + + for (int refresh_times = 0; refresh_times <= max_refresh_times && !need_exit(); ++refresh_times) { + for (int page = 0; page < MaxNumSupportListPages && !need_exit(); ++page) { + // Step 1: 获取助战干员列表 + analyzer.set_image(ctrler()->get_image()); + // 未识别到任何助战干员,极有可能当前不在助战列表界面,报错退出 + if (!analyzer.analyze(known_oper_names)) [[unlikely]] { + Log.error(__FUNCTION__, "| Fail to analyze support list"); + return false; + } + std::vector support_list = analyzer.get_result(); + + // Step 2: 筛选助战干员列表 + for (size_t index = 0; index < support_list.size(); ++index) { + const SupportUnit& support_unit = support_list[index]; + known_oper_names.insert(support_unit.name); + + // 若 support_unit 满足以下筛选条件: + // 1. 当 max_spec_lvl == true 时,精英化等级达到 2; + // 2. 当 allow_non_friend_support_unit == false 时,必须满足 support_unit.from_friend == true; + // 且存在 filtered_required_opers[i] 与之匹配: + // 1. filtered_required_opers[i].name == name; + // 2. support_unit.elite >= filtered_required_opers[i].skill - 1; + // 则将 candidates[i] 设置为 index。 + if (max_spec_lvl && support_unit.elite == 2) { + continue; + } + if (!allow_non_friend_support_unit && !support_unit.from_friend) { + continue; + } + + // 若 role == Role::Unknown 则选择这名干员,使用其默认技能 + if (role == Role::Unknown) { + return try_use_support_unit_with_skill(support_unit, 0); + } + + for (size_t i = 0; i < filtered_required_opers.size(); ++i) { + const RequiredOper& required_oper = filtered_required_opers[i]; + if (role == required_oper.role && support_unit.name == required_oper.name && + support_unit.elite >= required_oper.skill - 1) { + candidates[i] = index; + } + } + } + + // Step 3: 依次点选筛选出的助战干员,根据需要判断技能是否为专三,并使用 + for (size_t i = 0; i < filtered_required_opers.size(); ++i) { + if (!candidates[i].has_value()) { + continue; + } + const RequiredOper& required_oper = filtered_required_opers[i]; + const SupportUnit& support_unit = support_list[candidates[i].value()]; + if (try_use_support_unit_with_skill(support_unit, required_oper.skill, max_spec_lvl)) { + return true; + } + } + + // 重置 candidate + candidates.assign(filtered_required_opers.size(), std::nullopt); + // 未滑到尾页,滑到下一页 + if (page < MaxNumSupportListPages - 1) { + ProcessTask(*this, { "UseSupportUnit-SwipeToTheRight" }).run(); + } + } // inner for loop to iterate through support list pages + // 重新定位到助战列表最左侧第一页 + for (int i = 0; i < MaxNumSupportListPages - 1; ++i) { + ProcessTask(*this, { "UseSupportUnit-SwipeToTheLeft" }).run(); + } + // 更新助战列表 + if (refresh_times < max_refresh_times) { + ProcessTask(*this, { "UseSupportUnit-RefreshSupportList" }).run(); + } + } // outer for loop to iterate until reaching refresh_times + return false; +} + +bool asst::UseSupportUnitTaskPlugin::try_use_support_unit_with_skill( + const SupportUnit& support_unit, + int skill, + bool max_spec_lvl) +{ + LogTraceFunction; + + // 点选被选择的助战干员 + ctrler()->click(support_unit.rect); + sleep(Config.get_options().task_delay); + ProcessTask(*this, { "Stop@LoadingText", "Stop" }).run(); + + if (skill != 0) { + if (max_spec_lvl) { + // 判断所需技能是否为专三 + Matcher max_spec_lvl_analyzer(ctrler()->get_image()); + max_spec_lvl_analyzer.set_task_info("UseSupportUnit-MaxSpecLvl-" + std::to_string(skill)); + if (!max_spec_lvl_analyzer.analyze()) { // 所需技能非专三 + // 取消选择 + ProcessTask(*this, { "UseSupportUnit-Cancel" }).run(); + return false; + } + } + // 选择技能 + ProcessTask(*this, { "UseSupportUnit-SelectSkill-" + std::to_string(skill) }).run(); + } + + // 确认选择 + ProcessTask(*this, { "UseSupportUnit-Confirm" }).run(); + + // callback + json::value info = basic_info_with_what("RecruitSuppportOperator"); + info["details"]["name"] = support_unit.name; + info["details"]["skill"] = skill; + callback(AsstMsg::SubTaskExtraInfo, info); + + return true; +} diff --git a/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.h b/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.h new file mode 100644 index 0000000000..c4d61d1b9c --- /dev/null +++ b/src/MaaCore/Task/Miscellaneous/UseSupportUnitTaskPlugin.h @@ -0,0 +1,81 @@ +// 助战干员选择插件 +#pragma once + +#include "Common/AsstBattleDef.h" +#include "Task/AbstractTask.h" +#include "Vision/Battle/SupportListAnalyzer.h" + +namespace asst +{ +class UseSupportUnitTaskPlugin : public AbstractTask +{ +public: + using AbstractTask::AbstractTask; + virtual ~UseSupportUnitTaskPlugin() override = default; + + using Role = battle::Role; + using RequiredOper = battle::RequiredOper; + using SupportUnit = SupportListAnalyzer::SupportUnit; + + bool try_add_support_unit( + const std::vector& required_opers = {}, + int max_refresh_times = 0, + bool max_spec_lvl = true, + bool allow_non_friend_support_unit = false); + +protected: + virtual bool _run() override { return true; }; + +private: + /// + /// 在职业 role 的助战列表中寻找一名列于 required_opers 中的干员并使用其指定技能; + /// 若 role == Role::Unknown 则忽视 required_opers,在当前职业的助战列表中随机选择一名干员并使用其默认技能。 + /// + /// 所需助战干员的职业。 + /// + /// 所需助战干员列表,当且仅当 role != Role::Unknown 时有效,默认其中的干员都属于职业 role。 + /// + /// 最大刷新助战列表的次数。 + /// 是否要求技能专三。 + /// 是否允许使用非好友助战干员。 + /// + /// 若成功找到并使用所需的助战干员,则返回 true,反之则返回 false。 + /// + /// + /// 默认已经点开助战列表; + /// 每次只能识别一页助战列表,因此最多会识别 MaxNumSupportListPages * (max_refresh_times + 1) 次; + /// 若识别到多个满足条件的干员,则优先选择在 required_opers 中排序靠前的干员; + /// 当 role == Role::Unknown 时因没有指定技能,max_spec_lvl == true 仅会要求助战干员的专精等级达到 2。 + /// + bool try_add_support_unit_for_role( + Role role = Role::Unknown, + const std::vector& required_opers = {}, + int max_refresh_times = 0, + bool max_spec_lvl = true, + bool allow_non_friend_support_unit = false); + + /// + /// 点选助战干员,判断技能是否为专三,并使用其 skill 技能; + /// 若 int == 0 则忽视 max_spec_lvl,并使用 support_unit 的默认技能。 + /// + /// 目标助战干员。 + /// 目标技能。 + /// 是否要求技能专三。 + /// + /// 若成功使用助战干员,则返回 true,反之则返回 false。 + /// + /// + /// 默认目标技能存在且已解锁。 + /// + bool try_use_support_unit_with_skill(const SupportUnit& support_unit, int skill = 0, bool max_spec_lvl = true); + + /// + /// 助战列表的总页数。 + /// + /// + /// 助战列表共有 9 个栏位,一页即一屏,屏幕上最多只能同时完整显示 8 名助战干员,因而总页数为 2。 + /// 注意助战列表页之间的内容重叠,可以基于“助战列表中不会有重复名字的干员”的假设优化。 + /// + static constexpr int MaxNumSupportListPages = 2; +}; +} // namespace asst diff --git a/src/MaaCore/Vision/Battle/SupportListAnalyzer.cpp b/src/MaaCore/Vision/Battle/SupportListAnalyzer.cpp new file mode 100644 index 0000000000..7fce85dd9a --- /dev/null +++ b/src/MaaCore/Vision/Battle/SupportListAnalyzer.cpp @@ -0,0 +1,95 @@ +#include "SupportListAnalyzer.h" + +#include "Config/TaskData.h" +#include "Utils/NoWarningCV.h" +#include "Vision/Matcher.h" +#include "Vision/MultiMatcher.h" +#include "Vision/RegionOCRer.h" + +bool asst::SupportListAnalyzer::analyze(const std::unordered_set& ignored_oper_names) +{ + LogTraceFunction; + + MultiMatcher flag_analyzer(m_image); + flag_analyzer.set_task_info("SupportListAnalyzer-Flag"); + if (!flag_analyzer.analyze()) { + return false; + } + + const auto& name_task_ptr = Task.get("SupportListAnalyzer-NameOcr"); + const auto& elite_task_ptr = Task.get("SupportListAnalyzer-Elite"); + const auto& level_task_ptr = Task.get("SupportListAnalyzer-LevelOcr"); + + RegionOCRer ocr_analyzer(m_image); + Matcher elite_analyzer(m_image); + + std::vector results; + for (const auto& [rect, score, templ_name] : flag_analyzer.get_result()) { +#ifdef ASST_DEBUG + cv::rectangle(m_image_draw, make_rect(rect), cv::Scalar(0, 255, 0), 2); +#endif + + // 干员名称 + const Rect name_roi = rect.move(name_task_ptr->roi); +#ifdef ASST_DEBUG + cv::rectangle(m_image_draw, make_rect(name_roi), cv::Scalar(0, 255, 0), 2); +#endif + + ocr_analyzer.set_roi(name_roi); + ocr_analyzer.set_replace(name_task_ptr->replace_map, name_task_ptr->replace_full); + if (!ocr_analyzer.analyze()) { + continue; + } + + const auto& name = ocr_analyzer.get_result().text; + // 如果干员名称出现在 ignored_oper_names 则忽视这个干员 + if (ignored_oper_names.contains(name)) { + continue; + } + + // 干员精英化等级 + const Rect elite_roi = rect.move(elite_task_ptr->roi); +#ifdef ASST_DEBUG + cv::rectangle(m_image_draw, make_rect(elite_roi), cv::Scalar(0, 255, 0), 2); +#endif + + elite_analyzer.set_task_info(elite_task_ptr); + elite_analyzer.set_roi(elite_roi); + int elite = 0; + if (elite_analyzer.analyze()) { + elite = (elite_analyzer.get_result().templ_name == "SupportListAnalyzer-Elite-2" ? 2 : 1); + } + + // 干员等级 + const Rect level_roi = rect.move(level_task_ptr->roi); +#ifdef ASST_DEBUG + cv::rectangle(m_image_draw, make_rect(level_roi), cv::Scalar(0, 255, 0), 2); +#endif + + ocr_analyzer.set_roi(level_roi); + ocr_analyzer.set_replace(level_task_ptr->replace_map, level_task_ptr->replace_full); + if (!ocr_analyzer.analyze()) { + continue; + } + int level = 0; + if (!utils::chars_to_number(ocr_analyzer.get_result().text, level)) { + continue; + } + Log.info(ocr_analyzer.get_result().text, level); + + SupportUnit support_unit { + .rect = rect, + .name = name, + .elite = elite, + .level = level, + .from_friend = (templ_name == "SupportListAnalyzer-Friend.png"), + }; + LogInfo << __FUNCTION__ << "| Found support operator from" + << (support_unit.from_friend ? "friend:" : "non-friend:") << support_unit.name << "with elite" + << support_unit.elite << "and level" << support_unit.level; + results.emplace_back(std::move(support_unit)); + } + + m_result = std::move(results); + return !m_result.empty(); +} diff --git a/src/MaaCore/Vision/Battle/SupportListAnalyzer.h b/src/MaaCore/Vision/Battle/SupportListAnalyzer.h new file mode 100644 index 0000000000..b92bb00750 --- /dev/null +++ b/src/MaaCore/Vision/Battle/SupportListAnalyzer.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Vision/VisionHelper.h" + +namespace asst +{ +class SupportListAnalyzer final : public VisionHelper +{ +public: + using VisionHelper::VisionHelper; + virtual ~SupportListAnalyzer() noexcept override = default; + + struct SupportUnit // 备选助战干员 + { + Rect rect; + std::string name; // 干员名称/代号 + int elite = 0; // 精英化阶段 + int level = 0; // 等级 + bool from_friend = false; // 是否为好友助战 + // SomeType modules // 模组 + // ———————— 以下字段仅在集成战略中有效 ———————— + int hope = 0; // 希望消耗 + int elite_after_promotion = 0; // 进阶后精英化阶段,仅在集成战略中有效, + int level_after_promotion = 0; // 进阶后等级,仅在集成战略中有效, + }; + + /// + /// 识别 m_image 中显示的助战列表页,忽视名字出现在 ignored_oper_names 中的干员。 + /// + /// 需要被忽视的干员的名字。 + /// + /// 助战列表共有 9 个栏位,一页即一屏,屏幕上最多只能同时完整显示 8 名助战干员。 + /// 基于“助战列表中不会有重复名字的干员”的假设,ignored_oper_names 参数用于筛除助战列表页之间的内容重叠。 + /// + /// + /// 返回识别到的助战干员列表。 + /// + bool analyze(const std::unordered_set& ignored_oper_names = {}); + + [[nodiscard]] std::vector get_result() const { return m_result; }; + +private: + std::vector m_result; +}; +} diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index 4d23577f22..89898149c3 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -1372,6 +1372,13 @@ namespace MaaWpfGui.Main break; } + case "RecruitSuppportOperator": + { + var name = subTaskDetails!["name"]!.ToString(); + Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RecruitSuppportOperator"), name), UiLogColor.Info); + break; + } + case "RecruitTagsSelected": { var selected = subTaskDetails["tags"] ?? new JArray(); diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml index f8feb36212..a0d5b15440 100644 --- a/src/MaaWpfGui/Res/Localizations/en-us.xaml +++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml @@ -728,6 +728,7 @@ The video aspect ratio needs to be 16:9 without interference factors such as bla Abort upload to Penguin Statistics No bonus stage, stopped Operators of the following operator groups are missing: + Add support unit: {0} Stage Queue: Unable to use PRTS Mission started diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml index 41cbc4d9dc..84527fe9e5 100644 --- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml +++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml @@ -731,6 +731,7 @@ C:\\leidian\\LDPlayer9 Penguin-Statsへのアップロードをあきらめました ステージ報酬がないため停止します 次のオペレーターがオペレーターグループにありません: + サポートユニットを追加:{0} ステージキュー: 自動指揮利用不可 行動開始しました diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml index 4ab90b6dab..ba8f5dc763 100644 --- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml +++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml @@ -731,6 +731,7 @@ C:\\leidian\\LDPlayer9 펭귄 물류에 업로드 불가 EX 스테이지가 없어 중단되었습니다 다음 오퍼레이터들이 누락되었습니다: + 지원 유닛 추가: {0} 스테이지 대기열: 프록시 명령을 사용할 수 없습니다 행동 개시 diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml index 1d0fba1d03..71b29d2b3e 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml @@ -730,6 +730,7 @@ C:\\leidian\\LDPlayer9。\n 放弃上传企鹅物流 无奖励关卡,已停止 缺少以下干员组中的干员: + 编入助战干员:{0} 关卡队列: 无法使用代理指挥 已开始行动 diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml index 05282e0d4e..cc1315a995 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml @@ -730,6 +730,7 @@ C:\\leidian\\LDPlayer9。\n 放棄上傳企鵝物流 無獎勵關卡,已停止 缺少以下幹員組的幹員: + 編入助戰幹員:{0} 關卡隊列: 無法使用代理指揮 已開始行動 diff --git a/tools/TaskSorter/TaskSorter.py b/tools/TaskSorter/TaskSorter.py index 1c8a188034..cd763170d8 100644 --- a/tools/TaskSorter/TaskSorter.py +++ b/tools/TaskSorter/TaskSorter.py @@ -6,6 +6,8 @@ from pathlib import Path def sort_tasks(res: dict[str, any]): # 暂时只对 Roguelike 和 Reclamation 任务进行类字典序排序,其他任务保持原有相对顺序 classified_lists = { + "UseSupportUnit...": [], + "...@UseSupportUnit...": [], "Roguelike...": [], "Roguelike@...": [], "Phantom@Roguelike...": [], @@ -22,6 +24,8 @@ def sort_tasks(res: dict[str, any]): unclassified_list = [] classify_rules: list[tuple[str, list]] = [ + (r"^UseSupportUnit", classified_lists["UseSupportUnit..."]), + (r"^(\w+)@UseSupportUnit", classified_lists["...@UseSupportUnit..."]), (r"^Roguelike@", classified_lists["Roguelike@..."]), (r"^Roguelike", classified_lists["Roguelike..."]), (r"^Phantom@Roguelike", classified_lists["Phantom@Roguelike..."]),