diff --git a/resource/tasks.json b/resource/tasks.json
index c63b12a0b5..c0cfc5cab8 100644
--- a/resource/tasks.json
+++ b/resource/tasks.json
@@ -12727,24 +12727,62 @@
]
},
"Roguelike@StageTraderInvestConfirm": {
- "action": "ClickSelf",
+ "doc": "绑定 RoguelikeInvestTaskPlugin 插件进行投资,本任务仅作为进入投资系统时的初始化确认",
"roi": [
852,
422,
278,
144
],
- "postDelay": 500,
"exceededNext": [
"RoguelikeControlTaskPlugin-ExitThenStop"
],
"next": [
"Roguelike@StageTraderInvestSystemError",
- "Roguelike@StageTraderInvestSystemFull",
- "Roguelike@StageTraderInvestConfirm",
"Roguelike@StageTraderInvestCancel"
]
},
+ "Roguelike@StageTraderInvest-Arrow": {
+ "doc": "商店投资界面的箭头,99 >> 100",
+ "roi": [
+ 750,
+ 320,
+ 100,
+ 100
+ ]
+ },
+ "Roguelike@StageTraderInvest-Confirm": {
+ "algorithm": "JustReturn",
+ "action": "ClickRect",
+ "specificRect": [
+ 850,
+ 470,
+ 300,
+ 45
+ ]
+ },
+ "Roguelike@StageTraderInvest-Count": {
+ "doc": "商店投资界面的余额显示,99 >> 100",
+ "algorithm": "OcrDetect",
+ "isAscii": true,
+ "roi": [
+ 570,
+ 335,
+ 150,
+ 60
+ ]
+ },
+ "Roguelike@StageTraderInvest-Count-Error": {
+ "doc": "商店投资界面系统出错时余额显示",
+ "algorithm": "OcrDetect",
+ "isAscii": true,
+ "roi": [
+ 635,
+ 205,
+ 70,
+ 40
+ ]
+ },
"Roguelike@StageTraderInvestSystem": {
"templThreshold": 0.75,
"action": "ClickSelf",
diff --git a/resource/template/Roguelike@StageTraderInvest-Arrow.png b/resource/template/Roguelike@StageTraderInvest-Arrow.png
new file mode 100644
index 0000000000..8e331039db
Binary files /dev/null and b/resource/template/Roguelike@StageTraderInvest-Arrow.png differ
diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj
index ba260c16cf..2a423e8619 100644
--- a/src/MaaCore/MaaCore.vcxproj
+++ b/src/MaaCore/MaaCore.vcxproj
@@ -96,6 +96,7 @@
+
@@ -273,6 +274,7 @@
+
diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters
index b12b9bc4cc..90b08de49c 100644
--- a/src/MaaCore/MaaCore.vcxproj.filters
+++ b/src/MaaCore/MaaCore.vcxproj.filters
@@ -693,6 +693,9 @@
Source\Resource\TaskData
+
+ Source\Task\Roguelike
+
@@ -1154,5 +1157,8 @@
Source\Task\Infrast
+
+ Source\Task\Roguelike
+
\ No newline at end of file
diff --git a/src/MaaCore/Task/Interface/RoguelikeTask.cpp b/src/MaaCore/Task/Interface/RoguelikeTask.cpp
index d223624480..f73922fd3b 100644
--- a/src/MaaCore/Task/Interface/RoguelikeTask.cpp
+++ b/src/MaaCore/Task/Interface/RoguelikeTask.cpp
@@ -22,6 +22,7 @@
#include "Task/Roguelike/RoguelikeSkillSelectionTaskPlugin.h"
#include "Task/Roguelike/RoguelikeStageEncounterTaskPlugin.h"
#include "Task/Roguelike/RoguelikeStrategyChangeTaskPlugin.h"
+#include "Task/Roguelike/RoguelikeInvestTaskPlugin.h"
#include "Utils/Logger.hpp"
@@ -40,6 +41,7 @@ asst::RoguelikeTask::RoguelikeTask(const AsstCallback& callback, Assistant* inst
m_roguelike_task_ptr->register_plugin(m_roguelike_config_ptr);
m_debug_plugin_ptr = m_roguelike_task_ptr->register_plugin(m_roguelike_config_ptr);
m_roguelike_task_ptr->register_plugin(m_roguelike_config_ptr)->set_retry_times(0);
+ m_roguelike_task_ptr->register_plugin(m_roguelike_config_ptr);
m_custom_start_plugin_ptr =
m_roguelike_task_ptr->register_plugin(m_roguelike_config_ptr);
@@ -96,6 +98,8 @@ bool asst::RoguelikeTask::set_params(const json::value& params)
// 是否凹指定干员开局直升
m_roguelike_config_ptr->set_start_with_elite_two(params.get("start_with_elite_two", false));
m_roguelike_config_ptr->set_only_start_with_elite_two(params.get("only_start_with_elite_two", false));
+ m_roguelike_config_ptr->set_invest_maximum(params.get("investments_count", INT_MAX));
+ m_roguelike_config_ptr->set_invest_stop_when_full(params.get("stop_when_investment_full", false));
// 设置层数选点策略,相关逻辑在 RoguelikeStrategyChangeTaskPlugin
{
@@ -144,7 +148,6 @@ bool asst::RoguelikeTask::set_params(const json::value& params)
params.get("investment_enabled", true) ? INT_MAX : 0);
m_roguelike_task_ptr->set_times_limit("StageTraderRefreshWithDice",
params.get("refresh_trader_with_dice", false) ? INT_MAX : 0);
- m_roguelike_task_ptr->set_times_limit("StageTraderInvestConfirm", params.get("investments_count", INT_MAX));
if (params.get("stop_when_investment_full", false)) {
constexpr int InvestLimit = 999;
diff --git a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h
index 8b111c23b5..b2a623c002 100644
--- a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h
+++ b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h
@@ -55,6 +55,10 @@ namespace asst
bool get_start_with_elite_two() const { return m_start_with_elite_two; }
void set_only_start_with_elite_two(bool value) { m_only_start_with_elite_two = value; }
bool get_only_start_with_elite_two() const { return m_only_start_with_elite_two; }
+ void set_invest_maximum(int value) { m_invest_maximum = value; }
+ int get_invest_maximum() const { return m_invest_maximum; }
+ void set_invest_stop_when_full(bool value) { m_invest_stop_when_full = value; }
+ bool get_invest_stop_when_full() const { return m_invest_stop_when_full; }
private:
std::string m_theme; // 主题
@@ -62,6 +66,8 @@ namespace asst
int m_difficulty = 0; // 难度
bool m_start_with_elite_two = false; // 在刷开局模式下凹开局干员精二直升
bool m_only_start_with_elite_two = false; // 只凹开局干员精二直升且不进行作战
+ int m_invest_maximum = 0; // 投资次数上限
+ bool m_invest_stop_when_full = false; // 存款满了就停止
/* 以下为每次重置 */
public:
diff --git a/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.cpp
new file mode 100644
index 0000000000..18bf66e0b8
--- /dev/null
+++ b/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.cpp
@@ -0,0 +1,129 @@
+#include "RoguelikeInvestTaskPlugin.h"
+
+#include "Config/TaskData.h"
+#include "Controller/Controller.h"
+#include "Task/ProcessTask.h"
+#include "Utils/Logger.hpp"
+#include "Vision/RegionOCRer.h"
+
+bool asst::RoguelikeInvestTaskPlugin::verify(AsstMsg msg, const json::value& details) const
+{
+ if (msg != AsstMsg::SubTaskStart || details.get("subtask", std::string()) != "ProcessTask") {
+ return false;
+ }
+
+ const auto task_name = details.get("details", "task", "");
+ if (task_name.ends_with("Roguelike@StageTraderInvestConfirm")) {
+ return m_invest_count < m_config->get_invest_maximum();
+ }
+ else {
+ return false;
+ }
+}
+
+bool asst::RoguelikeInvestTaskPlugin::_run()
+{
+ LogTraceFunction;
+
+ auto image = ctrler()->get_image();
+
+ // 进入投资页面成功
+ // 999 >> 1000
+ // 取消 | 确认投资
+
+ int count = 0; // 当次已投资的个数
+ int retry = 0; // 重试次数
+ const auto ocr_current_count = [](const auto& img, const auto& task_name) -> std::optional {
+ RegionOCRer ocr(img);
+ ocr.set_task_info(task_name);
+ if (!ocr.analyze()) {
+ Log.error(__FUNCTION__, "unable to analyze current investment count.");
+ return std::nullopt;
+ };
+ int count = 0;
+ if (!utils::chars_to_number(ocr.get_result().text, count)) {
+ Log.error(__FUNCTION__, "unable to convert current investment count<", ocr.get_result().text,
+ "> to number.");
+ return std::nullopt;
+ }
+ return count;
+ };
+ // 当前的存款
+ auto deposit = ocr_current_count(image, "Roguelike@StageTraderInvest-Count");
+ // 可用投资次数
+ int count_limit = m_config->get_invest_maximum() - m_invest_count;
+ // 投资确认按钮
+ const auto& click_rect = Task.get("Roguelike@StageTraderInvest-Confirm")->specific_rect;
+ Log.info(__FUNCTION__, "开始投资, 存款", deposit.value_or(-1), ", 可投资次数", count_limit);
+ while (!need_exit() && deposit && *deposit < 999 && count_limit > 0 && retry < 3) {
+ int times = std::min(15, count_limit - count);
+ while (!need_exit() && times > 0) {
+ ctrler()->click(click_rect);
+ sleep(100);
+ times--;
+ }
+ image = ctrler()->get_image();
+ if (!is_investment_available(image)) { // 检查是否处于可投资状态
+ break; // 投资系统错误 / 没钱了
+ }
+ else if (auto ocr = ocr_current_count(image, "Roguelike@StageTraderInvest-Count"); ocr) {
+ // 可继续投资 / 到达投资上限999
+ if (*ocr == *deposit || *ocr > 999 || *ocr < 1) {
+ retry++; // 可能是出错了,重试三次放弃
+ }
+ else {
+ count += *ocr - *deposit;
+ deposit = *ocr;
+ retry = 0;
+ }
+ }
+ else {
+ Log.error(__FUNCTION__, "未知状态");
+ return true;
+ }
+ }
+
+ if (auto ocr = ocr_current_count(ctrler()->get_image(), "Roguelike@StageTrader-InvestCount"); ocr) {
+ // 可继续投资 / 到达投资上限999
+ count += *ocr - deposit.value_or(0);
+ deposit = *ocr;
+ }
+
+ const auto total = count + m_invest_count;
+
+ auto info = basic_info_with_what("RoguelikeInvestment");
+ info["details"]["count"] = count;
+ info["details"]["total"] = total;
+ info["details"]["deposit"] = deposit ? *deposit : -1;
+ callback(AsstMsg::SubTaskExtraInfo, info);
+
+ Log.info(__FUNCTION__, "本轮投资结束, 投资", count, ", 共投资", total, "; 系统余额", deposit ? *deposit : -1);
+ m_invest_count = total;
+
+ if (count_limit - count <= 0) {
+ Log.info(__FUNCTION__, "投资达到设置上限,", m_config->get_invest_maximum());
+ stop_roguelike();
+ m_task_ptr->set_enable(false);
+ return true;
+ }
+ if (deposit.value_or(0) == 999 && m_config->get_invest_stop_when_full()) {
+ Log.info(__FUNCTION__, "存款已满");
+ stop_roguelike();
+ m_task_ptr->set_enable(false);
+ }
+
+ return true;
+}
+
+bool asst::RoguelikeInvestTaskPlugin::is_investment_available(const cv::Mat& image) const
+{
+ auto task = ProcessTask(*this, { "Roguelike@StageTraderInvest-Arrow", "Roguelike@StageTraderInvestSystemError" });
+ task.set_reusable_image(image).set_retry_times(3);
+ task.set_times_limit("Roguelike@StageTraderInvestCancel", 0);
+ return task.run() && task.get_last_task_name() == "Roguelike@StageTraderInvest-Arrow";
+}
+
+void asst::RoguelikeInvestTaskPlugin::stop_roguelike()
+{
+ dynamic_cast(m_task_ptr)->set_times_limit("Roguelike@StageTraderInvestConfirm", 0);
+}
diff --git a/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.h b/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.h
new file mode 100644
index 0000000000..2819617567
--- /dev/null
+++ b/src/MaaCore/Task/Roguelike/RoguelikeInvestTaskPlugin.h
@@ -0,0 +1,21 @@
+#pragma once
+#include "AbstractRoguelikeTaskPlugin.h"
+
+namespace asst
+{
+ // 肉鸽投资插件
+ class RoguelikeInvestTaskPlugin : public AbstractRoguelikeTaskPlugin
+ {
+ public:
+ using AbstractRoguelikeTaskPlugin::AbstractRoguelikeTaskPlugin;
+ virtual ~RoguelikeInvestTaskPlugin() override = default;
+ virtual bool verify(AsstMsg msg, const json::value& details) const override;
+
+ private:
+ virtual bool _run() override;
+ bool is_investment_available(const cv::Mat& image) const;
+ void stop_roguelike();
+
+ int m_invest_count = 0;
+ };
+} // namespace asst
diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs
index e6becac79d..8f14c7e91b 100644
--- a/src/MaaWpfGui/Main/AsstProxy.cs
+++ b/src/MaaWpfGui/Main/AsstProxy.cs
@@ -1175,6 +1175,11 @@ namespace MaaWpfGui.Main
break;
}
+ case "RoguelikeInvestment":
+ Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RoguelikeInvestment"), subTaskDetails["count"], subTaskDetails["total"], subTaskDetails["deposit"]), UiLogColor.Info);
+ break;
+
+
case "RoguelikeSettlement":
// 肉鸽结算
bool roguelikeGamePass = (bool)subTaskDetails["game_pass"];
diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml
index 656e441f21..4788cc822b 100644
--- a/src/MaaWpfGui/Res/Localizations/en-us.xaml
+++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml
@@ -634,6 +634,7 @@ The video aspect ratio needs to be 16:9 without interference factors such as bla
Node: Emergency Operation
Node: Dreadful Foe
Event:
+ Invested {1}(+{0}), deposit: {2}
Roguelike Settlement: {0}\nExplore: {1} Floor {2} Step\nCombat: {3} / {4} Emergency / {5} BOSS\nRecruit: {6}\nCollectibles: {7}\nDifficulty: {8}\nScore: {9}\nExp: {10}, Skill: {11}
Current times
Nothing
diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml
index b7f5f2bea0..a185164368 100644
--- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml
+++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml
@@ -634,6 +634,7 @@ Bilibili: ログイン インターフェイスに表示されるアカウント
ステージ: 緊急作戦
ステージ: 悪路凶敵
イベント:
+ 投資済み: {1}(+{0})、デポジット: {2}
ローグライク決済: {0}\n探検する: {1} 床 {2} ステップ\n戦闘: {3} / {4} 緊急 / {5} BOSS\nリクルート: {6}\nコレクション: {7}\n困難: {8}\nスコア: {9}\n経験値: {10}, スキル: {11}
現在の回数
ドロップなし
diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml
index b00c9392cf..f5eb44cdf4 100644
--- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml
+++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml
@@ -634,6 +634,7 @@ Bilibili: 로그인 인터페이스에 표시되는 계정 이름(예: 장산)
레벨: 긴급 작전
레벨: 험난한 길
이벤트:
+ 투자: {1}(+{0}), 예금: {2}
로그라이크 종착점: {0}\n클리어 : {1} 구역 {2} 노드\n전투 횟수: {3} 일반 / {4} 정예 / {5} 리더\n모집 횟수 : {6} 회\n수집: {7}\n난이도: {8}\n점수: {9}\n경험치: {10}, 스킬: {11}
현재 횟수
없음
diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml
index 1aff3233bc..dcd328bec0 100644
--- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml
+++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml
@@ -634,6 +634,7 @@
关卡: 紧急作战
关卡: 险路恶敌
事件:
+ 已投资 {1}(+{0}), 存款: {2}
肉鸽结算: {0}\n探索: {1} 层 {2} 步\n战斗: {3}/{4} 紧急/{5} BOSS\n招募: {6} 收藏品: {7}\n难度: {8} 得分: {9}\nExp: {10} Skill: {11}
当前次数
无
diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml
index f2f2ac27eb..75bfcc6bda 100644
--- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml
+++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml
@@ -632,6 +632,7 @@
關卡: 緊急作戰
關卡: 險路惡敵
事件:
+ 已投資 {1}(+{0}), 存款: {2}
肉鴿結算: {0}\n探索: {1} 层 {2} 步\n戰鬥: {3}/{4} 緊急/{5} BOSS\n招募: {6} 收藏品: {7}\n難度: {8} 得分: {9}\nExp: {10} Skill: {11}
目前次數
無