diff --git a/resource/tasks/tasks.json b/resource/tasks/tasks.json
index 453cfe55e4..08405af76c 100644
--- a/resource/tasks/tasks.json
+++ b/resource/tasks/tasks.json
@@ -1253,6 +1253,11 @@
"action": "ClickRect",
"specificRect": [965, 200, 70, 50]
},
+ "FightSeries-List-Exceeded": {
+ "template": "FightSeries-List-Exceeded.png",
+ "roi": [965, 200, 70, 325],
+ "doc": "找到每一个需要补充理智的连战次数"
+ },
"StartButton1": {
"doc": "绑定插件调整连战次数",
"algorithm": "OcrDetect",
@@ -1520,6 +1525,7 @@
"next": ["CloseStonePage"]
},
"CloseStonePage": {
+ "Doc": "2025.5.5:next 在 FightSeriesAdjustPlugin 插件中被动态修改",
"algorithm": "JustReturn",
"action": "ClickRect",
"specificRect": [0, 200, 300, 400],
diff --git a/resource/template/FightSeries-List-Exceeded.png b/resource/template/FightSeries-List-Exceeded.png
new file mode 100644
index 0000000000..c7e05fe2d1
Binary files /dev/null and b/resource/template/FightSeries-List-Exceeded.png differ
diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj
index 7e69be6269..34c18b48ab 100644
--- a/src/MaaCore/MaaCore.vcxproj
+++ b/src/MaaCore/MaaCore.vcxproj
@@ -248,6 +248,7 @@
+
@@ -446,6 +447,7 @@
+
diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters
index a419bba7d0..ec4306b470 100644
--- a/src/MaaCore/MaaCore.vcxproj.filters
+++ b/src/MaaCore/MaaCore.vcxproj.filters
@@ -1328,6 +1328,9 @@
Source\Task\Roguelike
+
+ Source\Task\Fight
+
@@ -1842,6 +1845,9 @@
Source\Task\Roguelike
+
+ Source\Task\Fight
+
diff --git a/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.cpp b/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.cpp
new file mode 100644
index 0000000000..81cb864822
--- /dev/null
+++ b/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.cpp
@@ -0,0 +1,103 @@
+#include "FightSeriesAdjustPlugin.h"
+
+#include "Config/TaskData.h"
+#include "Controller/Controller.h"
+#include "Task/ProcessTask.h"
+#include "Utils/Logger.hpp"
+#include "Vision/MultiMatcher.h"
+#include "Vision/RegionOCRer.h"
+
+bool asst::FightSeriesAdjustPlugin::verify(AsstMsg msg, const json::value& details) const
+{
+ if (msg != AsstMsg::SubTaskCompleted || details.get("subtask", std::string()) != "ProcessTask") {
+ return false;
+ }
+
+ return details.get("details", "task", "").ends_with("CloseStonePage");
+}
+
+bool asst::FightSeriesAdjustPlugin::_run()
+{
+ LogTraceFunction;
+
+ auto task = ProcessTask(*this, { "FightSeries-Open" });
+ task.run();
+
+ int exceeded_num = get_exceeded_num();
+ if (exceeded_num <= 6 && exceeded_num > 1) {
+ ProcessTask(*this, { "FightSeries-List-" + std::to_string(exceeded_num - 1) }).run();
+ set_close_stone_page_next(true);
+ }
+ else {
+ set_close_stone_page_next(false);
+ }
+ return true;
+}
+
+int asst::FightSeriesAdjustPlugin::get_exceeded_num() const
+{
+ const std::vector> list_y_ranges = {
+ { 505, 505 + 55 }, // List-1: [505, 560)
+ { 445, 445 + 55 }, // List-2: [445, 500)
+ { 383, 383 + 55 }, // List-3: [383, 438)
+ { 320, 320 + 55 }, // List-4: [320, 375)
+ { 260, 260 + 55 }, // List-5: [260, 315)
+ { 200, 200 + 50 } // List-6: [200, 250)
+ };
+
+ auto img = ctrler()->get_image();
+ if (img.empty()) {
+ return 0;
+ }
+ MultiMatcher multi_matcher(img);
+ multi_matcher.set_task_info("FightSeries-List-Exceeded");
+ if (!multi_matcher.analyze()) {
+ Log.info(__FUNCTION__, "Fight series exceeded analyze failed");
+ LogInfo << __FUNCTION__ << "Falling back to OCR for exceeded number detection.";
+
+ std::vector detected_numbers;
+
+ for (const auto& range : list_y_ranges) {
+ RegionOCRer analyzer(img);
+ analyzer.set_task_info("NumberOcrReplace");
+ analyzer.set_bin_threshold(150, 255);
+ analyzer.set_roi({ 980, range.first, 40, range.second - range.first });
+
+ auto result_opt = analyzer.analyze();
+ if (result_opt && !result_opt->text.empty()) {
+ try {
+ int number = std::stoi(result_opt->text);
+ detected_numbers.push_back(number);
+ }
+ catch (const std::invalid_argument&) {
+ LogInfo << __FUNCTION__ << "OCR failed to parse number: " << result_opt->text;
+ }
+ }
+ }
+
+ if (!detected_numbers.empty()) {
+ int max_number = *std::max_element(detected_numbers.begin(), detected_numbers.end());
+ LogInfo << __FUNCTION__ << "Detected max number via OCR: " << max_number;
+ return max_number + 1;
+ }
+
+ return -1;
+ }
+ auto match_result = multi_matcher.get_result();
+ if (match_result.empty()) {
+ LogInfo << __FUNCTION__ << "No matches found for FightSeries-List-Exceeded.";
+ return -1;
+ }
+ sort_by_vertical_(match_result);
+ int exceeded_y_pos = match_result.back().rect.y;
+ for (size_t i = 0; i < list_y_ranges.size(); ++i) {
+ const auto& range = list_y_ranges[i];
+ if (exceeded_y_pos >= range.first && exceeded_y_pos < range.second) {
+ int list_index = static_cast(i + 1); // 列表序号从 1 开始
+ LogInfo << __FUNCTION__ << "Exceeded icon y=" << exceeded_y_pos << " is within List-" << list_index
+ << " range [" << range.first << ", " << range.second << ").";
+ return list_index;
+ }
+ }
+ return 0;
+}
diff --git a/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.h b/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.h
new file mode 100644
index 0000000000..fe6f0b2978
--- /dev/null
+++ b/src/MaaCore/Task/Fight/FightSeriesAdjustPlugin.h
@@ -0,0 +1,32 @@
+#pragma once
+#include "Config/TaskData.h"
+#include "Task/AbstractTaskPlugin.h"
+
+namespace asst
+{
+class FightSeriesAdjustPlugin final : public AbstractTaskPlugin
+{
+public:
+ using AbstractTaskPlugin::AbstractTaskPlugin;
+ virtual ~FightSeriesAdjustPlugin() override = default;
+
+ virtual bool verify(AsstMsg msg, const json::value& details) const override;
+
+ void set_close_stone_page_next(bool enable) const
+ {
+ Task.get("Fight@CloseStonePage")->next =
+ enable ? modified_close_stone_page_next : original_close_stone_page_next;
+ };
+
+protected:
+ virtual bool _run() override;
+
+private:
+ // 临时处理
+ std::vector original_close_stone_page_next = { "Fight@StageSNReturnFlag", "Fight@Stop" };
+ std::vector modified_close_stone_page_next = { "Fight@StartButton1",
+ "Fight@StageSNReturnFlag",
+ "Fight@Stop" };
+ int get_exceeded_num() const;
+};
+}
diff --git a/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp b/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp
index 6cedcc4878..c890fc008e 100644
--- a/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp
+++ b/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp
@@ -38,3 +38,4 @@ bool asst::FightTimesTaskPlugin::_run()
inited = true;
return true;
}
+
diff --git a/src/MaaCore/Task/Interface/FightTask.cpp b/src/MaaCore/Task/Interface/FightTask.cpp
index de518f7d6e..24d50614b4 100644
--- a/src/MaaCore/Task/Interface/FightTask.cpp
+++ b/src/MaaCore/Task/Interface/FightTask.cpp
@@ -4,6 +4,7 @@
#include "Config/TaskData.h"
#include "Task/Fight/DrGrandetTaskPlugin.h"
+#include "Task/Fight/FightSeriesAdjustPlugin.h"
#include "Task/Fight/FightTimesTaskPlugin.h"
#include "Task/Fight/MedicineCounterTaskPlugin.h"
#include "Task/Fight/SanityBeforeStageTaskPlugin.h"
@@ -54,6 +55,8 @@ asst::FightTask::FightTask(const AsstCallback& callback, Assistant* inst) :
m_fight_times_task_plugin_prt = m_fight_task_ptr->register_plugin();
m_fight_times_task_plugin_prt->set_retry_times(3);
m_medicine_plugin = m_fight_task_ptr->register_plugin();
+ m_fight_series_adjust_plugin_ptr = m_fight_task_ptr->register_plugin();
+ m_fight_series_adjust_plugin_ptr->set_retry_times(3);
m_subtasks.emplace_back(m_start_up_task_ptr);
m_subtasks.emplace_back(m_stage_navigation_task_ptr);
@@ -70,11 +73,23 @@ bool asst::FightTask::set_params(const json::value& params)
const int expiring_medicine = params.get("expiring_medicine", 0);
const int stone = params.get("stone", 0);
const int times = params.get("times", INT_MAX);
- const int series = params.get("series", 1);
- if (series < 1 || series > 6) {
+ int series = params.get("series", 1);
+
+ // 重置插件状态 1000 表示自动连战,-1 表示禁用连战切换
+ m_fight_times_task_plugin_prt->set_enable(series != -1);
+ m_fight_series_adjust_plugin_ptr->set_close_stone_page_next(false);
+ m_fight_series_adjust_plugin_ptr->set_enable(series == 1000);
+ if (series == 1000) {
+ series = 6;
+ }
+ else if (series == -1) {
+ series = 1;
+ }
+ else if (series < 1 || series > 6) {
Log.error("Invalid series");
return false;
}
+
bool enable_penguin = params.get("report_to_penguin", false);
bool enable_yituliu = params.get("report_to_yituliu", false);
std::string penguin_id = params.get("penguin_id", "");
diff --git a/src/MaaCore/Task/Interface/FightTask.h b/src/MaaCore/Task/Interface/FightTask.h
index 6b0848c4cd..9372c64c02 100644
--- a/src/MaaCore/Task/Interface/FightTask.h
+++ b/src/MaaCore/Task/Interface/FightTask.h
@@ -12,6 +12,7 @@ class StageNavigationTask;
class DrGrandetTaskPlugin;
class SideStoryReopenTask;
class MedicineCounterTaskPlugin;
+class FightSeriesAdjustPlugin;
class FightTask final : public InterfaceTask
{
@@ -28,6 +29,7 @@ protected:
std::shared_ptr m_stage_navigation_task_ptr = nullptr;
std::shared_ptr m_fight_task_ptr = nullptr;
std::shared_ptr m_fight_times_task_plugin_prt = nullptr;
+ std::shared_ptr m_fight_series_adjust_plugin_ptr = nullptr;
std::shared_ptr m_medicine_plugin = nullptr;
std::shared_ptr m_stage_drops_plugin_ptr = nullptr;
std::shared_ptr m_dr_grandet_task_plugin_ptr = nullptr;
diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs
index 359a8e2807..ba7fcce20f 100644
--- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs
+++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs
@@ -103,10 +103,17 @@ public class FightSettingsUserControlModel : TaskViewModel
{ "炭", "SK-5" },
};
- ///
- /// Gets or private sets the list of series.
- ///
- public List SeriesList { get; private set; } = Enumerable.Range(1, 6).ToList();
+ public Dictionary SeriesList { get; } = new()
+ {
+ { "AUTO", 1000 },
+ { "6", 6 },
+ { "5", 5 },
+ { "4", 4 },
+ { "3", 6 },
+ { "2", 2 },
+ { "1", 1 },
+ { LocalizationHelper.GetString("NotSelected"), -1 },
+ };
public string?[] Stages => [Stage1, Stage2, Stage3];
@@ -465,7 +472,7 @@ public class FightSettingsUserControlModel : TaskViewModel
}
}
- private int _series = ConfigurationHelper.GetValue(ConfigurationKeys.SeriesQuantity, 1);
+ private int _series = ConfigurationHelper.GetValue(ConfigurationKeys.SeriesQuantity, 1000);
///
/// Gets or sets the max number of times.
diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml
index b84e8637c9..0970cca5f6 100644
--- a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml
+++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml
@@ -170,7 +170,9 @@
IsHitTestVisible="{c:Binding !FightTaskRunning,
Source={x:Static helper:Instances.TaskQueueViewModel}}"
ItemsSource="{Binding SeriesList}"
- SelectedValue="{Binding Series}" />
+ SelectedValue="{Binding Series}"
+ DisplayMemberPath="Key"
+ SelectedValuePath="Value" />