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} 目前次數