From dce6fbfbce4baf608709bd9323f0cc458f5bdae7 Mon Sep 17 00:00:00 2001 From: travellerse <58685479+travellerse@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:54:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=B7=E5=B8=B8?= =?UTF-8?q?=E4=B9=90=E8=8A=82=E7=82=B9=E7=AD=96=E7=95=A5=20(#13868)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 支持识别常乐节点的子类型 * perf: 将树洞节点优先级拆到配置里 * refactor: 将界园树洞策略从通用Routing策略中分离出来 * feat: 初步实现刷常乐节点 * feat: 为刷常乐节点功能添加UI * refactor: 重命名节点类型获取函数 * perf: 优化节点类型的日志输出 * feat: 添加发现目标常乐节点后退出 * i18n: CN常乐节点显示文本 * fix: 修复type2name映射初始化潜在问题 * perf: 添加目标子类型的设置和获取方法 * refactor: 优化优先级节点类型解析逻辑 * feat: 刷常乐节点策略下保存路上的招募券 * i18n: 为其它语言提供占位 * perf: 将新增的本地化键值放到一起 * chore: 调整日志输出中的空格 * perf: 仅在非开局招募时保存招募券 * perf: 保留招募券到达上限时正常招募 * fix: 修复gui日志输出没有使用本地化 * fix: 给界园树洞节点点击加上200ms的前置延时 * perf: 在 RoguelikeRoutingTaskPlugin 中添加运行模式以区分普通地图与界园树洞 * refactor: 优化RoguelikeBoskyPassageMap的单例使用 * feat: 添加界园树洞节点类型wpf输出及其颜色配置 * fix: 不在每次开始肉鸽重置m_target_subtype --- resource/tasks/Roguelike/JieGarden.json | 104 +++++ resource/tasks/Roguelike/base.json | 3 + ...en@Roguelike@ReserveRecruitmentVoucher.png | Bin 0 -> 2121 bytes .../Config/Roguelike/RoguelikeMapConfig.cpp | 30 +- .../Roguelike/Map/AbstractRoguelikeMap.cpp | 72 ++++ .../Task/Roguelike/Map/AbstractRoguelikeMap.h | 16 +- .../Map/RoguelikeBoskyPassageMap.cpp | 39 +- .../Roguelike/Map/RoguelikeBoskyPassageMap.h | 32 +- .../Map/RoguelikeRoutingTaskPlugin.cpp | 380 +++++++++++------- .../Map/RoguelikeRoutingTaskPlugin.h | 20 +- .../Task/Roguelike/RoguelikeConfig.cpp | 12 + src/MaaCore/Task/Roguelike/RoguelikeConfig.h | 16 +- .../Roguelike/RoguelikeRecruitTaskPlugin.cpp | 9 + .../RoguelikeStageEncounterTaskPlugin.cpp | 30 ++ .../Single/MaaTask/RoguelikeTask.cs | 31 ++ src/MaaWpfGui/Constants/ConfigurationKeys.cs | 1 + src/MaaWpfGui/Constants/UILogColor.cs | 32 ++ .../Models/AsstTasks/AsstRoguelikeTask.cs | 10 + src/MaaWpfGui/Res/Localizations/en-us.xaml | 14 + src/MaaWpfGui/Res/Localizations/ja-jp.xaml | 14 + src/MaaWpfGui/Res/Localizations/ko-kr.xaml | 14 + src/MaaWpfGui/Res/Localizations/zh-cn.xaml | 14 + src/MaaWpfGui/Res/Localizations/zh-tw.xaml | 14 + src/MaaWpfGui/Res/Themes/Dark.xaml | 15 + src/MaaWpfGui/Res/Themes/Light.xaml | 15 + .../RoguelikeSettingsUserControlModel.cs | 70 ++++ .../RoguelikeSettingsUserControl.xaml | 11 + 27 files changed, 816 insertions(+), 202 deletions(-) create mode 100644 resource/template/Roguelike/JieGarden/JieGarden@Roguelike@ReserveRecruitmentVoucher.png create mode 100644 src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.cpp diff --git a/resource/tasks/Roguelike/JieGarden.json b/resource/tasks/Roguelike/JieGarden.json index c2e7764213..a123c21536 100644 --- a/resource/tasks/Roguelike/JieGarden.json +++ b/resource/tasks/Roguelike/JieGarden.json @@ -506,6 +506,94 @@ "template": "empty.png", "specialParams": [188, 107, 608, 321, 1028, 535, 64, 57, 140, 107, 15] }, + "JieGarden@Roguelike@RoutingBoskyPassagePriority_Default": { + "doc": [ + "界园树洞节点优先级策略配置", + "按优先级从高到低排列节点类型名称", + "拾遗>传说>故肆>易与>筹谋>常乐>杂疑(暂时取消)>灾祸(暂时取消)" + ], + "template": "empty.png", + "next": [ + "JieGarden@Roguelike@MapNodeOmissions", + "JieGarden@Roguelike@MapNodeLegend", + "JieGarden@Roguelike@MapNodeOldShop", + "JieGarden@Roguelike@MapNodeYiTrader", + "JieGarden@Roguelike@MapNodeScheme", + "JieGarden@Roguelike@MapNodePlaytime" + ] + }, + "JieGarden@Roguelike@RoutingBoskyPassagePriority_FindPlaytime": { + "doc": ["常乐>拾遗>传说>故肆>易与>筹谋>杂疑(暂时取消)>灾祸(暂时取消)"], + "template": "empty.png", + "next": [ + "JieGarden@Roguelike@MapNodePlaytime", + "JieGarden@Roguelike@MapNodeOmissions", + "JieGarden@Roguelike@MapNodeLegend", + "JieGarden@Roguelike@MapNodeOldShop", + "JieGarden@Roguelike@MapNodeYiTrader", + "JieGarden@Roguelike@MapNodeScheme" + ] + }, + "JieGarden@Roguelike@MapNodeYiTrader": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageYiTraderEnter"] + }, + "JieGarden@Roguelike@MapNodeDisaster": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageCombatDpsEnter"] + }, + "JieGarden@Roguelike@MapNodeOmissions": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@MapNodeLegend": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@MapNodeOldShop": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@MapNodeScheme": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@MapNodePlaytime": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@MapNodeDoubts": { + "template": "empty.png", + "next": ["JieGarden@Roguelike@RoutingAction-StageEncounterEnter"] + }, + "JieGarden@Roguelike@ReserveRecruitmentVoucher": { + "doc": "保留招募券 - hsv用来区分是否到达保留上限", + "algorithm": "MatchTemplate", + "method": "HSVCount", + "roi": [948, 5, 229, 56], + "colorScales": [ + [ + [0, 0, 50], + [10, 255, 210] + ], + [ + [170, 0, 50], + [180, 255, 210] + ], + [ + [0, 0, 230], + [180, 30, 255] + ] + ], + "action": "ClickSelf", + "next": ["JieGarden@Roguelike@ReserveRecruitmentVoucherConfirm", "#self"] + }, + "JieGarden@Roguelike@ReserveRecruitmentVoucherConfirm": { + "algorithm": "OcrDetect", + "text": ["确认留存"], + "roi": [766, 572, 142, 49], + "action": "ClickSelf" + }, "JieGarden@Roguelike@RoutingBoskyPassageNodeAnalyze": { "template": [ "JieGarden@Roguelike@MapNodeOldShop.png", @@ -1195,6 +1283,22 @@ "binThreshold": [200, 255], "useRaw": false }, + "JieGarden@Roguelike@StrategyChange_mode20001": { + "text": ["_default", "_restart", "_boskyPassageDefault"], + "ocrReplace": [ + ["洪陆楼", "_default"], + ["山水阁", "_restart"], + ["云(瓦)?(亭|亨)", "_restart"], + ["汝吾(门)?", "_restart"], + ["见字祠", "_restart"], + ["始末陵", "_restart"], + ["是非境", "_boskyPassageDefault"] + ], + "roi": [627, 14, 71, 28], + "withoutDet": true, + "binThreshold": [200, 255], + "useRaw": false + }, "JieGarden@Roguelike@StrategyChange_mode4": { "text": ["_default", "_exit", "_leaveBoskyPassage"], "ocrReplace": [ diff --git a/resource/tasks/Roguelike/base.json b/resource/tasks/Roguelike/base.json index 175947a170..2f9e3b0848 100644 --- a/resource/tasks/Roguelike/base.json +++ b/resource/tasks/Roguelike/base.json @@ -1742,6 +1742,9 @@ "Roguelike@StrategyChange_mode10001": { "baseTask": "Roguelike@StrategyChange_default" }, + "Roguelike@StrategyChange_mode20001": { + "baseTask": "Roguelike@StrategyChange_default" + }, "Roguelike@StrategyChange_mode4": { "baseTask": "Roguelike@StrategyChange_default" }, diff --git a/resource/template/Roguelike/JieGarden/JieGarden@Roguelike@ReserveRecruitmentVoucher.png b/resource/template/Roguelike/JieGarden/JieGarden@Roguelike@ReserveRecruitmentVoucher.png new file mode 100644 index 0000000000000000000000000000000000000000..97ec5b26f682796523983b180d12ab6987ee85a8 GIT binary patch literal 2121 zcmV-P2)6f$P)Px#32;bRa{vGi!~g&e!~vBn4jTXf2kuEkK~#9!?V9;l8d(;{`yZc)ao>$w+PEjt z7&S4jV~m+3)16N1s8E7JL9tjOxT2_lDCl>%#l!nh5b@}w&uQ=Z{D624Reaui_ulu4 zU(l!ss&Y`0gD0l!{G&jg<)AJHE!OOG*|HMvQw&G1Uqq z>#mn*vNF#l+9i5zX_D%h(;@Ye<59;YN%g^LVs}Q{`ogUJ@Lb|r)KfgF|9`7@FY3hI z)t8!3d{63&h{DvD5;S|i5bO87M?07GGA5S=EU#@YF6m=GdoUz zBjAN5{`J;}x>XeAR$pp{_Z(Y5yEDJ~noQjd63tfTSHTVHF~0`@{sczd_oTi_5!6Xk zGZ|m0EX^h;UU|vIfQ}nNjibSqp!0aB%Ef@&^zy{E)(je)ec<-lSlhqeX^tu5c z4)nNCp)i%E6s-R19@JB~&KS3ZC&8>4IAa)ap~Fga8oabIi$MU`0meP2!|El+W2}CE zTDwG6@B8jab6@ITXR40?XAlv-s-DwJtAoTvfOZ!aBLMK{5iNqrZ2(w`pf+RH(-Qe{ zi2oi7c0iE;R%2KSGp1v~@B3)8)1B|2;F$CY^}YmM;)1y;mUbf(+QX~wbzx8!X20#D z+l>)F0IXf0Nu_lFVEKfS#`xA%^)tH&0x^g<1IH;iVt{f&xrkRufid7kwL-T#tYc)I z8C@55Za&6@ALAQ>DT4_gy6i%I1O>#(!NaSsRLF}Qap3oZTdJoda|k2^8Ij1u8bf1R zeX~kM<&~EfnavoYO<>fIJ}=&^F#}$~D;J(wvzOThMXcB909sbEm)VXq@7*-_=Yn|S zq+Gv{PB>$pWQtFWx)1~ciL=l8T@y~TB507PHZgKOEjMG_k8gWIoaF>#0ZeQP^(`u1 zUsLahdP-MEswKCge7Xv?_qs&b8wnCzE8CA=ur2D*~!(sKU zD_mx?b~wW>q~hbo74^{}6_!vcug1Tu%VpN6vBW*Azn<%P61!ab#7`xQwB;NAT7utB;L1MYf#Z4Uap2 zf_hr=B|<&5o2@GP)`a@{RZL{4C*6fcub0)2_|tE?>7sLo@Bska;JasGM+! zG80T`PjA;yyu{7^2`K&@M3t4^k?C>c92ARTG_3IUqTFCePbEO=f0}H!N;r#$*psUL zv>)xOtln~rAP@qMLD!7}N10tfRMa_o5u^VmzWEAQ->L{2CF+?8=C-b?|H|Bl`sa3>gJUU-=PHKRIoHdF3ggDNR7pKeWE!wS zA`HCSK)0Chto~OOZ4Pv5M3;v7Ljbfb8d^t>OVGH2>ip`dx9L#fiHjvW=oad`SJ7ka zVEY}!hzD0@mnC#I?mqpXC5cYCqLZ%hnDb%P)9U`Xg?H-+Nx6Pd&#u1OicUA0RB^&= z4kHSD+MxmS0U;?h1qiVqBEWA4*ak)a8nrRbI6m*uD!t3Sm$a(q;AAtUgJy(!zEK^@!3A?`ChOo=ZxL!Zb?MFh+l=;%g9J z_RywM%9bCYl$niF6oFAESsz)hwV+!=rIL~ND=c`U)4_e8E^Vf+sqc2tDE4{jlT(+Q z*`XcQcY2eqE4=4B! NodeTypeMapping = { - { "CombatOps", RoguelikeNodeType::CombatOps }, - { "EmergencyOps", RoguelikeNodeType::EmergencyOps }, - { "DreadfulFoe", RoguelikeNodeType::DreadfulFoe }, - { "Encounter", RoguelikeNodeType::Encounter }, - { "Boons", RoguelikeNodeType::Boons }, - { "SafeHouse", RoguelikeNodeType::SafeHouse }, - { "Recreation", RoguelikeNodeType::Recreation }, - { "RogueTrader", RoguelikeNodeType::RogueTrader }, - { "RegionalCommissions", RoguelikeNodeType::RegionalCommissions }, - { "LostAndFound", RoguelikeNodeType::LostAndFound }, - { "Scout", RoguelikeNodeType::Scout }, - { "BoskyPassage", RoguelikeNodeType::BoskyPassage }, - { "Prophecy", RoguelikeNodeType::Prophecy }, - { "MysteriousPresage", RoguelikeNodeType::MysteriousPresage }, - { "FerociousPresage", RoguelikeNodeType::FerociousPresage }, - { "IdeaFilter", RoguelikeNodeType::IdeaFilter }, - { "FaceOff", RoguelikeNodeType::FaceOff }, - { "Legend", RoguelikeNodeType::Legend }, - { "Omissions", RoguelikeNodeType::Omissions }, - { "Doubts", RoguelikeNodeType::Doubts }, - { "Disaster", RoguelikeNodeType::Disaster }, - { "Playtime", RoguelikeNodeType::Playtime }, - { "OldShop", RoguelikeNodeType::OldShop }, - { "YiTrader", RoguelikeNodeType::YiTrader }, - { "Scheme", RoguelikeNodeType::Scheme } - }; - const std::string theme = json.at("theme").as_string(); m_templ_type_mappings.erase(theme); for (const auto& node_json : json.at("node").as_array()) { - const RoguelikeNodeType node_type = NodeTypeMapping.at(node_json.at("type").as_string()); + const RoguelikeNodeType node_type = name2type(node_json.at("type").as_string()); for (const auto& template_json : node_json.at("template").as_array()) { const std::string node_template = template_json.as_string(); m_templ_type_mappings[theme].emplace(node_template, node_type); diff --git a/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.cpp b/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.cpp new file mode 100644 index 0000000000..2dd07e3a62 --- /dev/null +++ b/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.cpp @@ -0,0 +1,72 @@ +#include "AbstractRoguelikeMap.h" +#include "RoguelikeBoskyPassageMap.h" + +#include + +namespace asst +{ + +// 共用的节点类型映射表 +static const std::unordered_map NodeTypeMapping = { + { "CombatOps", RoguelikeNodeType::CombatOps }, + { "EmergencyOps", RoguelikeNodeType::EmergencyOps }, + { "DreadfulFoe", RoguelikeNodeType::DreadfulFoe }, + { "Encounter", RoguelikeNodeType::Encounter }, + { "Boons", RoguelikeNodeType::Boons }, + { "SafeHouse", RoguelikeNodeType::SafeHouse }, + { "Recreation", RoguelikeNodeType::Recreation }, + { "RogueTrader", RoguelikeNodeType::RogueTrader }, + { "RegionalCommissions", RoguelikeNodeType::RegionalCommissions }, + { "LostAndFound", RoguelikeNodeType::LostAndFound }, + { "Scout", RoguelikeNodeType::Scout }, + { "BoskyPassage", RoguelikeNodeType::BoskyPassage }, + { "Prophecy", RoguelikeNodeType::Prophecy }, + { "MysteriousPresage", RoguelikeNodeType::MysteriousPresage }, + { "FerociousPresage", RoguelikeNodeType::FerociousPresage }, + { "IdeaFilter", RoguelikeNodeType::IdeaFilter }, + { "FaceOff", RoguelikeNodeType::FaceOff }, + { "Legend", RoguelikeNodeType::Legend }, + { "Omissions", RoguelikeNodeType::Omissions }, + { "Doubts", RoguelikeNodeType::Doubts }, + { "Disaster", RoguelikeNodeType::Disaster }, + { "Playtime", RoguelikeNodeType::Playtime }, + { "OldShop", RoguelikeNodeType::OldShop }, + { "YiTrader", RoguelikeNodeType::YiTrader }, + { "Scheme", RoguelikeNodeType::Scheme } +}; + +RoguelikeNodeType name2type(const std::string& node_name) +{ + auto it = NodeTypeMapping.find(node_name); + return it != NodeTypeMapping.end() ? it->second : RoguelikeNodeType::Unknown; +} + +std::string type2name(RoguelikeNodeType node_type) +{ + static const std::unordered_map reverse_mapping = []() { + std::unordered_map mapping; + for (const auto& [name, type] : NodeTypeMapping) { + mapping[type] = name; + } + return mapping; + }(); + + auto it = reverse_mapping.find(node_type); + return it != reverse_mapping.end() ? it->second : "Unknown"; +} + +std::string subtype2name(RoguelikeBoskySubNodeType sub_node_type) +{ + // 界园树洞子节点类型映射表 + static const std::unordered_map subtype_mapping = { + { RoguelikeBoskySubNodeType::Unknown, "Unknown" }, + { RoguelikeBoskySubNodeType::Ling, "Ling" }, // 令 - 常乐 掷地有声 + { RoguelikeBoskySubNodeType::Shu, "Shu" }, // 黍 - 常乐 种因得果 + { RoguelikeBoskySubNodeType::Nian, "Nian" } // 年 - 常乐 三缺一 + }; + + auto it = subtype_mapping.find(sub_node_type); + return it != subtype_mapping.end() ? it->second : "Unknown"; +} + +} // namespace asst diff --git a/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.h b/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.h index 52ceea7a8a..7b3649997b 100644 --- a/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.h +++ b/src/MaaCore/Task/Roguelike/Map/AbstractRoguelikeMap.h @@ -1,10 +1,12 @@ #pragma once -#include -#include +#include namespace asst { + +enum class RoguelikeBoskySubNodeType; + enum class RoguelikeNodeType { Init = -1, // Init @@ -39,4 +41,14 @@ enum class RoguelikeNodeType YiTrader = 24, // 易与 Scheme = 25 // 筹谋 }; + +// 根据字符串获取节点类型 +RoguelikeNodeType name2type(const std::string& node_name); + +// 获取节点类型的字符串名称 +std::string type2name(RoguelikeNodeType node_type); + +// 获取树篱之途子节点类型的字符串名称 +std::string subtype2name(RoguelikeBoskySubNodeType sub_node_type); + } // namespace asst diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.cpp b/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.cpp index 45b0c1b072..52c42e4155 100644 --- a/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.cpp +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.cpp @@ -1,21 +1,16 @@ #include "RoguelikeBoskyPassageMap.h" +#include "AbstractRoguelikeMap.h" #include "Utils/Logger.hpp" #include namespace asst { - -RoguelikeBoskyPassageMap::RoguelikeBoskyPassageMap() -{ - reset(); -} - std::optional RoguelikeBoskyPassageMap::create_and_insert_node(int x, int y, RoguelikeNodeType type, bool is_open) { if (!in_bounds(x, y)) { - Log.warn(__FUNCTION__, "| Coordinates (", x, ", ", y, ") out of bounds"); + Log.warn(__FUNCTION__, "| Coordinates (", x, ",", y, ") out of bounds"); return std::nullopt; } @@ -36,7 +31,7 @@ std::optional return idx; } else { - Log.warn(__FUNCTION__, "| Node already exists at (", x, ", ", y, ")"); + Log.warn(__FUNCTION__, "| Node already exists at (", x, ",", y, ")"); return std::nullopt; } } @@ -68,11 +63,35 @@ void RoguelikeBoskyPassageMap::set_node_type(size_t index, RoguelikeNodeType typ n->type = type; } +void RoguelikeBoskyPassageMap::set_node_subtype(size_t index, RoguelikeBoskySubNodeType sub_type) +{ + Node* n = get_valid_node(index); + if (!n || !n->exists) { + Log.warn(__FUNCTION__, "| Invalid index or node doesn't exist:", index); + return; + } + n->sub_type = sub_type; + Log.info(__FUNCTION__, "| Set node", index, "subtype to", subtype2name(sub_type)); +} + +RoguelikeBoskySubNodeType RoguelikeBoskyPassageMap::get_node_subtype(size_t index) const +{ + if (index >= m_nodes.size()) { + return RoguelikeBoskySubNodeType::Unknown; + } + const Node& n = m_nodes[index]; + if (!n.exists) { + return RoguelikeBoskySubNodeType::Unknown; + } + return n.sub_type; +} + void RoguelikeBoskyPassageMap::reset() { for (auto& n : m_nodes) { n = Node {}; // reset } + m_curr_pos = 0; m_existing_count = 0; } @@ -213,10 +232,10 @@ std::optional RoguelikeBoskyPassageMap::ensure_node_from_pixel( } auto [gx, gy] = pixel_to_grid_coords(px, py, config); - Log.info(__FUNCTION__, "| analyzing node (", px, ", ", py, ") -> (", gx, ", ", gy, ")"); + Log.info(__FUNCTION__, "| analyzing node (", px, ",", py, ") -> (", gx, ",", gy, ")"); if (!in_bounds(gx, gy)) { - Log.warn(__FUNCTION__, "| Grid coordinates (", gx, ", ", gy, ") out of bounds"); + Log.warn(__FUNCTION__, "| Grid coordinates (", gx, ",", gy, ") out of bounds"); return std::nullopt; } diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.h b/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.h index 92a4c9d060..1ffe1c4540 100644 --- a/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.h +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeBoskyPassageMap.h @@ -1,6 +1,7 @@ #pragma once #include "AbstractRoguelikeMap.h" +#include "Utils/SingletonHolder.hpp" #include #include @@ -8,10 +9,18 @@ namespace asst { -class RoguelikeBoskyPassageMap + +enum class RoguelikeBoskySubNodeType +{ + Unknown = 0, + Ling = 1, // 令 - 常乐 掷地有声 + Shu = 2, // 黍 - 常乐 种因得果 + Nian = 3, // 年 - 常乐 三缺一 +}; + +class RoguelikeBoskyPassageMap : public SingletonHolder { public: - RoguelikeBoskyPassageMap(); ~RoguelikeBoskyPassageMap() = default; // ===================== 创建/更新 ===================== @@ -24,6 +33,8 @@ public: void set_node_type(size_t index, RoguelikeNodeType type); + void set_curr_pos(size_t index) { m_curr_pos = index; } + void reset(); // ===================== 获取信息 ===================== @@ -39,6 +50,8 @@ public: // 全部开放节点(包含已通关) [[nodiscard]] std::vector get_open_nodes(std::optional type_filter = std::nullopt) const; + [[nodiscard]] size_t get_curr_pos() const { return m_curr_pos; } + [[nodiscard]] RoguelikeNodeType get_node_type(size_t index) const; [[nodiscard]] bool get_node_exists(size_t index) const; [[nodiscard]] bool get_node_open(size_t index) const; @@ -102,6 +115,18 @@ public: bool is_open, RoguelikeNodeType type = RoguelikeNodeType::Unknown); + // 设置子类型 + void set_node_subtype(size_t index, RoguelikeBoskySubNodeType sub_type); + + // 获取子类型 + [[nodiscard]] RoguelikeBoskySubNodeType get_node_subtype(size_t index) const; + + // 设置目标子类型 + void set_target_subtype(RoguelikeBoskySubNodeType target) { m_target_subtype = target; } + + // 获取目标子类型 + [[nodiscard]] RoguelikeBoskySubNodeType get_target_subtype() const { return m_target_subtype; } + // 中心节点 index static constexpr int CENTER_X = 3; static constexpr int CENTER_Y = 2; @@ -117,13 +142,16 @@ private: struct Node { RoguelikeNodeType type = RoguelikeNodeType::Unknown; + RoguelikeBoskySubNodeType sub_type = RoguelikeBoskySubNodeType::Unknown; bool exists = false; // 是否已创建 bool is_open = false; // 是否开放可探索 bool visited = false; // 是否已访问 }; std::array m_nodes {}; // 直接按 y*WIDTH + x 索引 + size_t m_curr_pos = 0; size_t m_existing_count = 0; + RoguelikeBoskySubNodeType m_target_subtype = RoguelikeBoskySubNodeType::Unknown; // 辅助函数:获取有效的节点引用,如果索引无效或节点不存在则返回 nullptr [[nodiscard]] Node* get_valid_node(size_t index); diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp index ac4dbcdb82..3e8c9b52d9 100644 --- a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp @@ -58,10 +58,17 @@ bool asst::RoguelikeRoutingTaskPlugin::load_params([[maybe_unused]] const json:: m_routing_strategy = RoutingStrategy::FastInvestment_JieGarden; return true; } - else { - m_routing_strategy = RoutingStrategy::BoskyPassage_JieGarden; + + if (mode == RoguelikeMode::FindPlaytime) { + m_bosky_routing_strategy = RoutingStrategy::FindPlaytime_JieGarden; + int target = m_config->get_find_playTime_target(); + RoguelikeBoskyPassageMap::get_instance().set_target_subtype(static_cast(target)); + Log.info(__FUNCTION__, "| FindPlaytime mode enabled with target:", target); return true; } + + m_bosky_routing_strategy = RoutingStrategy::BoskyPassage_JieGarden; + return true; } if (mode == RoguelikeMode::FastPass && squad == "蓝图测绘分队") { @@ -75,7 +82,7 @@ bool asst::RoguelikeRoutingTaskPlugin::load_params([[maybe_unused]] const json:: void asst::RoguelikeRoutingTaskPlugin::reset_in_run_variables() { m_map.reset(); - m_bosky_map.reset(); + RoguelikeBoskyPassageMap::get_instance().reset(); m_need_generate_map = true; m_selected_column = 0; m_selected_x = 0; @@ -88,6 +95,17 @@ bool asst::RoguelikeRoutingTaskPlugin::verify(const AsstMsg msg, const json::val } std::string task_name = details.get("details", "task", ""); + Log.debug(__FUNCTION__, "| Checking task:", task_name); + + // 检查是否包含 BoskyPassage 并设置运行模式 + if (task_name.find("JieGarden@Roguelike@Routing-BoskyPassage") != std::string::npos) { + m_run_mode = RoguelikeRoutingTaskRunMode::BoskyPassage_JieGarden; + Log.debug(__FUNCTION__, "| Setting run mode to BoskyPassage_JieGarden"); + } + else { + m_run_mode = RoguelikeRoutingTaskRunMode::Default; + Log.debug(__FUNCTION__, "| Setting run mode to Default"); + } // trigger 任务的名字可以为 "...@Roguelike@Routing-..." 的形式 if (const size_t pos = task_name.find('-'); pos != std::string::npos) { @@ -95,6 +113,7 @@ bool asst::RoguelikeRoutingTaskPlugin::verify(const AsstMsg msg, const json::val } if (task_name == m_config->get_theme() + "@Roguelike@Routing") { + Log.info(__FUNCTION__, "| RoguelikeRoutingTaskPlugin triggered for task:", task_name); return true; } @@ -105,102 +124,135 @@ bool asst::RoguelikeRoutingTaskPlugin::_run() { LogTraceFunction; - switch (m_routing_strategy) { - case RoutingStrategy::FastInvestment_Sarkaz: - if (m_need_generate_map) { - // 随机点击一个第一列的节点,先随便写写,垃圾代码迟早要重构 - ProcessTask(*this, { "Sarkaz@Roguelike@Routing-CombatOps" }).run(); - // 刷新节点 - ProcessTask(*this, { m_config->get_theme() + "@Roguelike@RoutingRefreshNode" }).run(); - // 不识别了,进商店,Go! - Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingAction-StageTraderEnter"); - // 偷懒,直接用 m_need_generate_map 判断是否已进过商店 - m_need_generate_map = false; - } - else { - Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingAction-ExitThenAbandon"); - } - break; - case RoutingStrategy::FastInvestment_JieGarden: - if (m_need_generate_map) { - cv::Mat image = ctrler()->get_image(); - cv::Mat image_draw = image.clone(); - update_map(image, RoguelikeMap::INIT_INDEX + 1, image_draw); -#ifdef ASST_DEBUG - const std::filesystem::path& relative_dir = utils::path("debug") / utils::path("roguelikeMap"); - const auto relative_path = relative_dir / (std::format("{}_draw.png", utils::format_now_for_filename())); - Log.debug(__FUNCTION__, "| Saving image to ", relative_path); - asst::imwrite(relative_path, image_draw); -#endif - m_need_generate_map = false; - } - if (m_map.get_curr_pos() == RoguelikeMap::INIT_INDEX) { - m_map.set_cost_fun([&](const RoguelikeNodePtr& node) { - if (node->type == RoguelikeNodeType::CombatOps || node->type == RoguelikeNodeType::EmergencyOps || - node->type == RoguelikeNodeType::DreadfulFoe) { - return 1; - } - return 0; - }); - m_map.update_node_costs(); - const size_t next_node = m_map.get_next_node(); + Log.info( + __FUNCTION__, + "| Running with routing_strategy:", + static_cast(m_routing_strategy), + "bosky_routing_strategy:", + static_cast(m_bosky_routing_strategy)); - // 若无法避免超过两场战斗则重开 - if (m_map.get_node_cost(next_node) >= 2) { - callback( - AsstMsg::TaskChainExtraInfo, - json::object { - { "what", "RoutingRestart" }, - { "why", "TooManyBattlesAhead" }, - { "node_cost", m_map.get_node_cost(next_node) }, - }); - - Task.set_task_base( - "JieGarden@Roguelike@RoutingAction", - "JieGarden@Roguelike@RoutingAction-ExitThenAbandon"); - reset_in_run_variables(); + if (m_run_mode == RoguelikeRoutingTaskRunMode::Default) { + switch (m_routing_strategy) { + case RoutingStrategy::FastInvestment_Sarkaz: + if (m_need_generate_map) { + // 随机点击一个第一列的节点,先随便写写,垃圾代码迟早要重构 + ProcessTask(*this, { "Sarkaz@Roguelike@Routing-CombatOps" }).run(); + // 刷新节点 + ProcessTask(*this, { m_config->get_theme() + "@Roguelike@RoutingRefreshNode" }).run(); + // 不识别了,进商店,Go! + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingAction-StageTraderEnter"); + // 偷懒,直接用 m_need_generate_map 判断是否已进过商店 + m_need_generate_map = false; } else { - const int next_node_x = m_origin_x; - const int next_node_y = m_map.get_node_y(next_node); - Point next_node_center = Point(next_node_x + m_node_width / 2, next_node_y + m_node_height / 2); - ctrler()->click(next_node_center); - sleep(200); - - Task.set_task_base( - "JieGarden@Roguelike@RoutingAction", - "JieGarden@Roguelike@RoutingAction-StageCombatOpsEnterThenLeave"); - m_map.set_curr_pos(next_node); + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingAction-ExitThenAbandon"); } - } - else { - // 执行默认的避战策略 - Task.set_task_base("JieGarden@Roguelike@RoutingAction", "JieGarden@Roguelike@Stages_default"); - } - break; - case RoutingStrategy::FastPass: - if (m_need_generate_map) { - generate_map(); - m_need_generate_map = false; - } + break; + case RoutingStrategy::FastInvestment_JieGarden: + if (m_need_generate_map) { + cv::Mat image = ctrler()->get_image(); + cv::Mat image_draw = image.clone(); + update_map(image, RoguelikeMap::INIT_INDEX + 1, image_draw); +#ifdef ASST_DEBUG + const std::filesystem::path& relative_dir = utils::path("debug") / utils::path("roguelikeMap"); + const auto relative_path = + relative_dir / (std::format("{}_draw.png", utils::format_now_for_filename())); + Log.debug(__FUNCTION__, "| Saving image to", relative_path); + asst::imwrite(relative_path, image_draw); +#endif + m_need_generate_map = false; + } + if (m_map.get_curr_pos() == RoguelikeMap::INIT_INDEX) { + m_map.set_cost_fun([&](const RoguelikeNodePtr& node) { + if (node->type == RoguelikeNodeType::CombatOps || node->type == RoguelikeNodeType::EmergencyOps || + node->type == RoguelikeNodeType::DreadfulFoe) { + return 1; + } + return 0; + }); + m_map.update_node_costs(); + const size_t next_node = m_map.get_next_node(); - m_selected_column = m_map.get_node_column(m_map.get_curr_pos()); - update_selected_x(); + // 若无法避免超过两场战斗则重开 + if (m_map.get_node_cost(next_node) >= 2) { + callback( + AsstMsg::TaskChainExtraInfo, + json::object { + { "what", "RoutingRestart" }, + { "why", "TooManyBattlesAhead" }, + { "node_cost", m_map.get_node_cost(next_node) }, + }); - refresh_following_combat_nodes(); - navigate_route(); - break; + Task.set_task_base( + "JieGarden@Roguelike@RoutingAction", + "JieGarden@Roguelike@RoutingAction-ExitThenAbandon"); + reset_in_run_variables(); + } + else { + const int next_node_x = m_origin_x; + const int next_node_y = m_map.get_node_y(next_node); + Point next_node_center = Point(next_node_x + m_node_width / 2, next_node_y + m_node_height / 2); + ctrler()->click(next_node_center); + sleep(200); - case RoutingStrategy::BoskyPassage_JieGarden: { - bosky_update_map(); - bosky_decide_and_click(); - break; + Task.set_task_base( + "JieGarden@Roguelike@RoutingAction", + "JieGarden@Roguelike@RoutingAction-StageCombatOpsEnterThenLeave"); + m_map.set_curr_pos(next_node); + } + } + else { + // 执行默认的避战策略 + Task.set_task_base("JieGarden@Roguelike@RoutingAction", "JieGarden@Roguelike@Stages_default"); + } + break; + case RoutingStrategy::FastPass: + if (m_need_generate_map) { + generate_map(); + m_need_generate_map = false; + } + + m_selected_column = m_map.get_node_column(m_map.get_curr_pos()); + update_selected_x(); + + refresh_following_combat_nodes(); + navigate_route(); + break; + + default: + break; + } } - default: - break; - } + if (m_run_mode == RoguelikeRoutingTaskRunMode::BoskyPassage_JieGarden) { + switch (m_bosky_routing_strategy) { + case RoutingStrategy::BoskyPassage_JieGarden: { + bosky_update_map(); + const std::vector priority_order = get_bosky_passage_priority("Default"); + bosky_decide_and_click(priority_order); + break; + } + case RoutingStrategy::FindPlaytime_JieGarden: { + // 更新地图 + bosky_update_map(); + const std::vector priority_order = get_bosky_passage_priority("FindPlaytime"); + + // 获取目标常乐节点子类型 + Log.info( + __FUNCTION__, + "| Looking for playtime subtype:", + subtype2name(RoguelikeBoskyPassageMap::get_instance().get_target_subtype())); + + // 尝试找到目标节点,使用常乐节点优先的策略 + bosky_decide_and_click(priority_order); + break; + } + + default: + break; + } + } return true; } @@ -225,7 +277,7 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_update_map() } MultiMatcher::ResultsVec match_results = node_analyzer.get_result(); - Log.info(__FUNCTION__, "| found ", match_results.size(), " nodes"); + Log.info(__FUNCTION__, "| found", match_results.size(), "nodes"); // 排序 靠左上优先 sort_by_vertical_(match_results); @@ -238,26 +290,27 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_update_map() // 处理每个识别到的节点 for (const auto& [rect, score, templ_name] : match_results) { - Log.debug(__FUNCTION__, "| analyzing node ", templ_name, " at (", rect.x, ", ", rect.y, ")"); + Log.debug(__FUNCTION__, "| analyzing node", templ_name, "at (", rect.x, ",", rect.y, ")"); const RoguelikeNodeType type = RoguelikeMapInfo.templ2type(theme, templ_name); if (type == RoguelikeNodeType::Unknown) { - Log.warn(__FUNCTION__, "| unknown template: ", templ_name); + Log.warn(__FUNCTION__, "| unknown template:", templ_name); continue; } // 检查是否为灰色节点 const bool is_open = templ_name.find("Grey") == std::string::npos; - auto idx = m_bosky_map.ensure_node_from_pixel(rect.x, rect.y, m_bosky_config, is_open, type); + auto idx = RoguelikeBoskyPassageMap::get_instance() + .ensure_node_from_pixel(rect.x, rect.y, m_bosky_config, is_open, type); if (idx.has_value()) { // 更新节点类型(防止类型不一致) - m_bosky_map.set_node_type(idx.value(), type); - Log.debug(__FUNCTION__, "| updated node (", idx.value(), ") type: (", static_cast(type), ")"); + RoguelikeBoskyPassageMap::get_instance().set_node_type(idx.value(), type); + Log.debug(__FUNCTION__, "| updated node (", idx.value(), ") type: (", type2name(type), ")"); } else { - Log.warn(__FUNCTION__, "| failed to create/update node from pixel (", rect.x, ", ", rect.y, ")"); + Log.warn(__FUNCTION__, "| failed to create/update node from pixel (", rect.x, ",", rect.y, ")"); } #ifdef ASST_DEBUG @@ -265,8 +318,9 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_update_map() cv::rectangle(image_draw, make_rect(rect), cv::Scalar(0, 0, 255), 2); cv::putText( image_draw, - std::to_string(static_cast(type)) + " (" + std::to_string(m_bosky_map.get_node_x(idx.value())) + - ", " + std::to_string(m_bosky_map.get_node_y(idx.value())) + ")", + std::to_string(static_cast(type)) + " (" + + std::to_string(RoguelikeBoskyPassageMap::get_instance().get_node_x(idx.value())) + ", " + + std::to_string(RoguelikeBoskyPassageMap::get_instance().get_node_y(idx.value())) + ")", cv::Point(rect.x, rect.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, @@ -279,47 +333,29 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_update_map() #ifdef ASST_DEBUG const std::filesystem::path& relative_dir = utils::path("debug") / utils::path("roguelikeMap"); const auto relative_path = relative_dir / (std::format("{}_bosky_draw.png", utils::format_now_for_filename())); - Log.debug(__FUNCTION__, "| Saving bosky map image to ", relative_path); + Log.debug(__FUNCTION__, "| Saving bosky map image to", relative_path); asst::imwrite(relative_path, image_draw); #endif - Log.info(__FUNCTION__, "| map updated with ", m_bosky_map.size(), " nodes"); + Log.info(__FUNCTION__, "| map updated with", RoguelikeBoskyPassageMap::get_instance().size(), "nodes"); } -void asst::RoguelikeRoutingTaskPlugin::bosky_decide_and_click() +void asst::RoguelikeRoutingTaskPlugin::bosky_decide_and_click(const std::vector& priority_order) { LogTraceFunction; Log.info(__FUNCTION__, "| deciding and clicking a bosky passage node"); - // 定义节点类型的优先级顺序 - const std::vector priority_order = { - RoguelikeNodeType::Omissions, // 拾遗 - 最高优先级 - RoguelikeNodeType::Legend, // 传说 - RoguelikeNodeType::YiTrader, // 易与 - RoguelikeNodeType::OldShop, // 故肆 - RoguelikeNodeType::Scheme, // 筹谋 - RoguelikeNodeType::Playtime, // 常乐 - RoguelikeNodeType::Doubts, // 杂疑 - // RoguelikeNodeType::Disaster // 祸乱 - 最低优先级 - }; - size_t chosen = 0; bool found = false; // 按优先级顺序查找可用的节点 for (const auto& node_type : priority_order) { - auto nodes_of_type = m_bosky_map.get_open_unvisited_nodes(node_type); + auto nodes_of_type = RoguelikeBoskyPassageMap::get_instance().get_open_unvisited_nodes(node_type); if (!nodes_of_type.empty()) { chosen = nodes_of_type.front(); found = true; - Log.debug( - __FUNCTION__, - "| found node of type (", - static_cast(node_type), - ") with index (", - chosen, - ")"); + Log.debug(__FUNCTION__, "| found node of type (", type2name(node_type), ") with index (", chosen, ")"); break; } } @@ -331,14 +367,14 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_decide_and_click() "JieGarden@Roguelike@RoutingAction-ClickRemainingCandleFlame"); return; } - int gx = m_bosky_map.get_node_x(chosen); - int gy = m_bosky_map.get_node_y(chosen); - RoguelikeNodeType node_type = m_bosky_map.get_node_type(chosen); + int gx = RoguelikeBoskyPassageMap::get_instance().get_node_x(chosen); + int gy = RoguelikeBoskyPassageMap::get_instance().get_node_y(chosen); + RoguelikeNodeType node_type = RoguelikeBoskyPassageMap::get_instance().get_node_type(chosen); - Log.info(__FUNCTION__, "| chosen node: ", chosen, " (", gx, ", ", gy, ") type: ", static_cast(node_type)); + Log.info(__FUNCTION__, "| chosen node:", chosen, "(", gx, ",", gy, ") type:", type2name(node_type)); // 点击节点中心 - auto [px, py] = m_bosky_map.get_node_pixel( + auto [px, py] = RoguelikeBoskyPassageMap::get_instance().get_node_pixel( chosen, m_bosky_config.origin_x, m_bosky_config.origin_y, @@ -346,38 +382,29 @@ void asst::RoguelikeRoutingTaskPlugin::bosky_decide_and_click() m_bosky_config.row_offset); if (px == -1 || py == -1) { - Log.error(__FUNCTION__, "| Invalid pixel coordinates for node ", chosen, ": (", px, ", ", py, ")"); + Log.error(__FUNCTION__, "| Invalid pixel coordinates for node", chosen, ": (", px, ",", py, ")"); return; } Point click_point(px + m_bosky_config.node_width / 2, py + m_bosky_config.node_height / 2); + sleep(200); ctrler()->click(click_point); - m_bosky_map.set_visited(chosen); + RoguelikeBoskyPassageMap::get_instance().set_visited(chosen); + RoguelikeBoskyPassageMap::get_instance().set_curr_pos(chosen); - // 根据节点类型判断 Task.set_task_base base_task_name - switch (m_bosky_map.get_node_type(chosen)) { - case RoguelikeNodeType::YiTrader: - Task.set_task_base("JieGarden@Roguelike@RoutingAction", "JieGarden@Roguelike@RoutingAction-StageYiTraderEnter"); - break; - case RoguelikeNodeType::Disaster: - Task.set_task_base( - "JieGarden@Roguelike@RoutingAction", - "JieGarden@Roguelike@RoutingAction-StageCombatDpsEnter"); - break; - case RoguelikeNodeType::Omissions: - case RoguelikeNodeType::Legend: - case RoguelikeNodeType::OldShop: - case RoguelikeNodeType::Scheme: - case RoguelikeNodeType::Playtime: - case RoguelikeNodeType::Doubts: - Task.set_task_base( - "JieGarden@Roguelike@RoutingAction", - "JieGarden@Roguelike@RoutingAction-StageEncounterEnter"); - break; - default: - Log.warn(__FUNCTION__, "| unknown node type: ", static_cast(node_type)); - break; - } + // 发送节点类型到 WPF + std::string node_type_name = type2name(node_type); + auto node_info = basic_info_with_what("BoskyPassageNode"); + node_info["details"]["node_type"] = node_type_name; + callback(AsstMsg::SubTaskExtraInfo, node_info); + + // 执行节点类型对应的任务 + const std::string& theme = m_config->get_theme(); + std::string node_name = type2name(node_type); + + const std::string node_task_name = theme + "@Roguelike@MapNode" + node_name; + // 设置 next + Task.set_task_base("JieGarden@Roguelike@RoutingAction", node_task_name); } bool asst::RoguelikeRoutingTaskPlugin::update_map( @@ -715,3 +742,52 @@ void asst::RoguelikeRoutingTaskPlugin::update_selected_x() m_selected_x = m_middle_x; } } + +std::vector + asst::RoguelikeRoutingTaskPlugin::get_bosky_passage_priority(const std::string& strategy) +{ + LogTraceFunction; + + const std::string& theme = m_config->get_theme(); + const std::string config_name = theme + "@Roguelike@RoutingBoskyPassagePriority_" + strategy; + + auto task_info = Task.get(config_name); + if (!task_info) { + Log.error(__FUNCTION__, "| priority config not found:", config_name); + return {}; + } + + // 从 next 字段中读取优先级配置 + const auto& next_tasks = task_info->next; + if (next_tasks.empty()) { + Log.warn(__FUNCTION__, "| Priority config is empty in:", config_name); + return {}; + } + + // 从任务名称中解析节点类型 + std::vector priority_order; + priority_order.reserve(next_tasks.size()); + + for (const std::string& task_name : next_tasks) { + // 解析类似 "JieGarden@Roguelike@MapNodeYiTrader" 这样的任务名 -> "YiTrader" + constexpr std::string_view map_node_prefix = "MapNode"; + const size_t pos = task_name.rfind(map_node_prefix); + if (pos == std::string::npos) { + Log.warn(__FUNCTION__, "| Invalid task name in priority config:", task_name); + continue; + } + + const std::string node_name = task_name.substr(pos + map_node_prefix.length()); + RoguelikeNodeType node_type = name2type(node_name); + if (node_type != RoguelikeNodeType::Unknown) { + priority_order.push_back(node_type); + Log.debug(__FUNCTION__, "| Added priority node type:", type2name(node_type), "from task:", task_name); + } + else { + Log.warn(__FUNCTION__, "| Failed to parse node type from task:", task_name); + } + } + + Log.info(__FUNCTION__, "| Loaded", priority_order.size(), "node types from priority config"); + return priority_order; +} diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h index 548d7f2a4c..9e222774aa 100644 --- a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h @@ -23,7 +23,14 @@ public: FastInvestment_Sarkaz, FastInvestment_JieGarden, FastPass, - BoskyPassage_JieGarden + BoskyPassage_JieGarden, + FindPlaytime_JieGarden + }; + + enum class RoguelikeRoutingTaskRunMode + { + Default, // 普通肉鸽地图 + BoskyPassage_JieGarden, // 界园树洞地图 }; protected: @@ -61,9 +68,11 @@ private: // ———————— constants and variables ——————————————————————————————————————————————— RoutingStrategy m_routing_strategy = RoutingStrategy::None; + RoutingStrategy m_bosky_routing_strategy = RoutingStrategy::None; RoguelikeMap m_map; - // 界园树洞平面地图 - RoguelikeBoskyPassageMap m_bosky_map; + // 界园树洞地图使用单例模式,通过 RoguelikeBoskyPassageMap::get_instance() 访问 + // 运行模式 + mutable RoguelikeRoutingTaskRunMode m_run_mode = RoguelikeRoutingTaskRunMode::Default; bool m_need_generate_map = true; size_t m_selected_column = 0; // 当前选中节点所在列 int m_selected_x = 0; // 当前选中节点的横坐标 (Rect.x) @@ -79,8 +88,9 @@ private: int m_direction_threshold = 0; // 节点间连线方向判定的阈值 // ==================== BoskyPassage (JieGarden) 专用 ==================== - void bosky_update_map(); // 从当前截图识别所有可见节点并更新/创建 - void bosky_decide_and_click(); // 策略 + void bosky_update_map(); // 从当前截图识别所有可见节点并更新/创建 + void bosky_decide_and_click(const std::vector& priority_order); // 策略,可指定优先级策略 + std::vector get_bosky_passage_priority(const std::string& strategy); // 从配置文件读取优先级 RoguelikeBoskyPassageMap::BoskyPassageMapConfig m_bosky_config; }; diff --git a/src/MaaCore/Task/Roguelike/RoguelikeConfig.cpp b/src/MaaCore/Task/Roguelike/RoguelikeConfig.cpp index 74dbdb1e68..0f10587f3e 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeConfig.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeConfig.cpp @@ -82,6 +82,10 @@ bool asst::RoguelikeConfig::verify_and_load_params(const json::value& params) // 启用特殊策略,联动 RoguelikeRoutingTaskPlugin Task.set_task_base(strategy_task, "JieGarden@Roguelike@StrategyChange_mode4-FastPass"); } + if (m_mode == RoguelikeMode::FindPlaytime) { + // 启用刷常乐节点策略,联动 RoguelikeRoutingTaskPlugin + Task.set_task_base(strategy_task, "JieGarden@Roguelike@StrategyChange_mode20001"); + } } } @@ -101,6 +105,14 @@ bool asst::RoguelikeConfig::verify_and_load_params(const json::value& params) m_run_for_collectible = true; // 烧开水模式下,如果不是只凹直升,第一轮游戏先烧水 } + if (m_mode == RoguelikeMode::FindPlaytime) { + m_find_playTime_target = params.get("find_playTime_target", 0); + if (m_find_playTime_target < 1 || m_find_playTime_target > 3) { + Log.error(__FUNCTION__, "| Invalid find_playTime_target", m_find_playTime_target); + return false; + } + } + return true; } diff --git a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h index 1a06824ecb..5fcd83c323 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h +++ b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h @@ -34,7 +34,10 @@ enum class RoguelikeMode CLP_PDS = 5, // 5 - 刷隐藏坍缩范式,以增加坍缩值为最优先目标 // ------------------ 萨卡兹主题专用模式 ------------------ - FastPass = 10001 // 10001 - 快速通过第一层 + FastPass = 10001, // 10001 - 快速通过第一层 + + // ------------------ 界园主题专用模式 ------------------ + FindPlaytime = 20001 // 20001 - 刷常乐节点,第一层进洞,找不到需要的节点就重开 }; struct RoguelikeOper @@ -92,7 +95,8 @@ public: return mode == RoguelikeMode::Exp || mode == RoguelikeMode::Investment || mode == RoguelikeMode::Collectible || mode == RoguelikeMode::Squad || mode == RoguelikeMode::Exploration || (mode == RoguelikeMode::CLP_PDS && theme == RoguelikeTheme::Sami) || - (mode == RoguelikeMode::FastPass && theme == RoguelikeTheme::Sarkaz); + (mode == RoguelikeMode::FastPass && theme == RoguelikeTheme::Sarkaz) || + (mode == RoguelikeMode::FindPlaytime && theme == RoguelikeTheme::JieGarden); } bool verify_and_load_params(const json::value& params); @@ -134,6 +138,11 @@ public: bool get_collectible_mode_shopping() const { return m_collectible_mode_shopping; } + // ------------------ 刷常乐节点模式 ------------------ + void set_find_playTime_target(int target) { m_find_playTime_target = target; } + + int get_find_playTime_target() const { return m_find_playTime_target; } + private: std::string m_theme; // 主题 RoguelikeMode m_mode = RoguelikeMode::Exp; // 模式 @@ -151,6 +160,9 @@ private: // ------------------ 刷开局模式 ------------------ bool m_collectible_mode_shopping = false; // 刷开局模式下进入商店时购物 + // ------------------ 刷常乐节点模式 ------------------ + int m_find_playTime_target = 0; // 目标常乐节点子类型 (1=令, 2=黍, 3=年) + private: // =========================== 萨米主题专用参数 =========================== public: diff --git a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp index 77f5e480af..7c45c9fe5b 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeRecruitTaskPlugin.cpp @@ -101,6 +101,15 @@ bool asst::RoguelikeRecruitTaskPlugin::_run() } } + // 刷常乐节点保存路上招募券 + // 只在非开局招募时保存招募券 + if (theme == RoguelikeTheme::JieGarden && mode == RoguelikeMode::FindPlaytime && !m_initail_recruit) { + bool ret = ProcessTask(*this, { "JieGarden@Roguelike@ReserveRecruitmentVoucher" }).run(); + if (ret) { + return true; + } + } + if (m_initail_recruit && m_recruit_count == 1) { if (m_config->get_use_support()) { // 是否使用助战干员开局 if (recruit_support_char()) { diff --git a/src/MaaCore/Task/Roguelike/RoguelikeStageEncounterTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeStageEncounterTaskPlugin.cpp index d9e72355ea..1b8a3e5cf1 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeStageEncounterTaskPlugin.cpp +++ b/src/MaaCore/Task/Roguelike/RoguelikeStageEncounterTaskPlugin.cpp @@ -3,6 +3,7 @@ #include "Config/Roguelike/RoguelikeStageEncounterConfig.h" #include "Controller/Controller.h" #include "Task/ProcessTask.h" +#include "Task/Roguelike/Map/RoguelikeBoskyPassageMap.h" #include "Utils/Logger.hpp" #include "Utils/NoWarningCV.h" #include "Vision/Matcher.h" @@ -131,6 +132,35 @@ std::optional asst::RoguelikeStageEncounterTaskPlugin::handle_singl } } + // 界园树洞 + if (m_config->get_theme() == RoguelikeTheme::JieGarden) { + auto& bosky_map = RoguelikeBoskyPassageMap::get_instance(); + if (event.name == "掷地有声") { + bosky_map.set_node_subtype(bosky_map.get_curr_pos(), RoguelikeBoskySubNodeType::Ling); + } + else if (event.name == "种因得果") { + bosky_map.set_node_subtype(bosky_map.get_curr_pos(), RoguelikeBoskySubNodeType::Shu); + } + else if (event.name == "三缺一") { + bosky_map.set_node_subtype(bosky_map.get_curr_pos(), RoguelikeBoskySubNodeType::Nian); + } + + if (bosky_map.get_target_subtype() != RoguelikeBoskySubNodeType::Unknown) { + if (bosky_map.get_node_subtype(bosky_map.get_curr_pos()) == bosky_map.get_target_subtype()) { + Log.info(__FUNCTION__, "| Found target playtime node, completing task and exiting"); + + auto target_info = basic_info_with_what("RoguelikeJieGardenTargetFound"); + target_info["details"]["target_subtype"] = subtype2name(bosky_map.get_target_subtype()); + callback(AsstMsg::SubTaskExtraInfo, target_info); + + // 完成任务,退出 + m_control_ptr->exit_then_stop(); + m_task_ptr->set_enable(false); + return std::nullopt; + } + } + } + const auto click_option_task_name = [&](int item, int total) { if (item > total) { Log.warn("Event:", event.name, "Total:", total, "Choice", item, "out of range, switch to choice", total); diff --git a/src/MaaWpfGui/Configuration/Single/MaaTask/RoguelikeTask.cs b/src/MaaWpfGui/Configuration/Single/MaaTask/RoguelikeTask.cs index 8a9718b1c9..7420e55b95 100644 --- a/src/MaaWpfGui/Configuration/Single/MaaTask/RoguelikeTask.cs +++ b/src/MaaWpfGui/Configuration/Single/MaaTask/RoguelikeTask.cs @@ -224,4 +224,35 @@ public enum RoguelikeMode /// 深入调查,尽可能稳定地打更多层数,不期而遇采用激进策略 /// Exploration = 7, + + /// + /// 20001 - 刷常乐节点,第一层进洞,找不到需要的节点就重开 + /// + FindPlaytime = 20001, +} + +/// +/// 界园常乐节点子类型 +/// +public enum RoguelikeBoskySubNodeType +{ + /// + /// 未知 + /// + Unknown = 0, + + /// + /// 令 - 常乐 掷地有声 + /// + Ling = 1, + + /// + /// 黍 - 常乐 种因得果 + /// + Shu = 2, + + /// + /// 年 - 常乐 三缺一 + /// + Nian = 3, } diff --git a/src/MaaWpfGui/Constants/ConfigurationKeys.cs b/src/MaaWpfGui/Constants/ConfigurationKeys.cs index a8de457062..b46b4e5b45 100644 --- a/src/MaaWpfGui/Constants/ConfigurationKeys.cs +++ b/src/MaaWpfGui/Constants/ConfigurationKeys.cs @@ -165,6 +165,7 @@ public static class ConfigurationKeys public const string RoguelikeMonthlySquadAutoIterate = "Roguelike.MonthlySquadAutoIterate"; public const string RoguelikeMonthlySquadCheckComms = "Roguelike.MonthlySquadCheckComms"; public const string RoguelikeDeepExplorationAutoIterate = "Roguelike.DeepExplorationAutoIterate"; + public const string RoguelikeFindPlaytimeTarget = "Roguelike.FindPlaytimeTarget"; public const string ReclamationTheme = "Reclamation.Theme"; public const string ReclamationMode = "Reclamation.Mode"; public const string ReclamationToolToCraft = "Reclamation.ToolToCraft"; diff --git a/src/MaaWpfGui/Constants/UILogColor.cs b/src/MaaWpfGui/Constants/UILogColor.cs index 9cec93a75d..0881657591 100644 --- a/src/MaaWpfGui/Constants/UILogColor.cs +++ b/src/MaaWpfGui/Constants/UILogColor.cs @@ -189,6 +189,38 @@ public static class UiLogColor /// public const string ExplorationAbandonedIS = "ExplorationAbandonedIS"; + // Bosky Passage Node Colors (JieGarden Theme) + + /// + /// The recommended color for Omissions node (拾遗). + /// + public const string BoskyOmissionsIS = "BoskyOmissionsIS"; + + /// + /// The recommended color for Legend node (传说). + /// + public const string BoskyLegendIS = "BoskyLegendIS"; + + /// + /// The recommended color for OldShop node (故肆). + /// + public const string BoskyOldShopIS = "BoskyOldShopIS"; + + /// + /// The recommended color for Scheme node (筹谋). + /// + public const string BoskySchemeIS = "BoskySchemeIS"; + + /// + /// The recommended color for Playtime node (常乐). + /// + public const string BoskyPlaytimeIS = "BoskyPlaytimeIS"; + + /// + /// The recommended color for Doubts node (杂疑). + /// + public const string BoskyDoubtsIS = "BoskyDoubtsIS"; + // 颜色在MaaWpfGui\Res\Themes中定义 // Brush are defined in MaaWpfGui\Res\Themes } diff --git a/src/MaaWpfGui/Models/AsstTasks/AsstRoguelikeTask.cs b/src/MaaWpfGui/Models/AsstTasks/AsstRoguelikeTask.cs index baf8e27c40..efd1b0c7bc 100644 --- a/src/MaaWpfGui/Models/AsstTasks/AsstRoguelikeTask.cs +++ b/src/MaaWpfGui/Models/AsstTasks/AsstRoguelikeTask.cs @@ -179,6 +179,11 @@ public class AsstRoguelikeTask : AsstBaseTask /// public bool DeepExplorationAutoIterate { get; set; } + /// + /// Gets or sets FindPlaytime 模式的目标常乐节点子类型 + /// + public RoguelikeBoskySubNodeType FindPlaytimeTarget { get; set; } = RoguelikeBoskySubNodeType.Ling; + /// /// Gets or sets a value indicating whether 是否在五层BOSS前停下来 /// @@ -253,6 +258,11 @@ public class AsstRoguelikeTask : AsstBaseTask taskParams["deep_exploration_auto_iterate"] = DeepExplorationAutoIterate; } + if (Mode == RoguelikeMode.FindPlaytime) + { + taskParams["find_playTime_target"] = (int)FindPlaytimeTarget; + } + if (SamiFirstFloorFoldartal && SamiStartFloorFoldartal.Length > 0) { taskParams["first_floor_foldartal"] = SamiStartFloorFoldartal; diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml index 17bdd71590..07b88be943 100644 --- a/src/MaaWpfGui/Res/Localizations/en-us.xaml +++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml @@ -166,6 +166,12 @@ To customize rotation schedules, please use 「{key=InfrastModeCustom}」.Squad to get collectibles Grind Monthly Squad, clear as far as possible Grind Deep Investigation, clear as far as possible + 刷常乐节点,第一层进洞,找不到需要的节点就重开 + 目标常乐节点 + 令 - 掷地有声 + 黍 - 种因得果 + 年 - 三缺一 + 发现目标常乐节点 {0} Starting Squad Starting Roles Starting {key=Operator} @@ -997,6 +1003,14 @@ Right-click to clear inactive tasks Node: Combat Operation Node: Emergency Operation Node: Dreadful Foe + 节点: 拾遗 + 节点: 传说 + 节点: 故肆 + 节点: 易与 + 节点: 筹谋 + 节点: 常乐 + 节点: 杂疑 + 节点: 灾祸 Event:  Event Name OCR Failed Foldartal Foresee:  diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml index badbb68777..60a412d313 100644 --- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml +++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml @@ -166,6 +166,12 @@ 支援選択を得るモードの分隊 月間小隊をグラインドし、できるだけ多くの層をクリア 深層探索をグラインドし、できるだけ多くの層をクリア + 刷常乐节点,第一层进洞,找不到需要的节点就重开 + 目标常乐节点 + 令 - 掷地有声 + 黍 - 种因得果 + 年 - 三缺一 + 发现目标常乐节点 {0} 最初の分隊 最初の職業 最初の{key=Operator} @@ -997,6 +1003,14 @@ C:\\leidian\\LDPlayer9 ステージ: 通常作戦 ステージ: 緊急作戦 ステージ: 悪路凶敵 + 节点: 拾遗 + 节点: 传说 + 节点: 故肆 + 节点: 易与 + 节点: 筹谋 + 节点: 常乐 + 节点: 杂疑 + 节点: 灾祸 イベント: イベント名認識失敗 予見啓示板: diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml index 5bbbff1303..95a3e3cf61 100644 --- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml +++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml @@ -166,6 +166,12 @@ 전기주전자 획득 분대 월간 분대 파밍, 최대한 많은 구역 클리어 심층 조사 파밍, 최대한 많은 구역 클리어 + 刷常乐节点,第一层进洞,找不到需要的节点就重开 + 目标常乐节点 + 令 - 掷地有声 + 黍 - 种因得果 + 年 - 三缺一 + 发现目标常乐节点 {0} 시작 분대 모집 조합 시작 {key=Operator} @@ -997,6 +1003,14 @@ C:\\leidian\\LDPlayer9 노드: 작전 노드: 긴급 작전 노드: 험난한 길 + 节点: 拾遗 + 节点: 传说 + 节点: 故肆 + 节点: 易与 + 节点: 筹谋 + 节点: 常乐 + 节点: 杂疑 + 节点: 灾祸 이벤트: 이벤트 인식 오류 암호판: diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml index 17a5900c4f..b09fef14ea 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml @@ -166,6 +166,12 @@ 烧水使用分队 刷月度小队,尽可能稳定地打更多层数 刷深入调查,尽可能稳定地打更多层数 + 刷常乐节点,第一层进洞,找不到需要的节点就重开 + 目标常乐节点 + 令 - 掷地有声 + 黍 - 种因得果 + 年 - 三缺一 + 发现目标常乐节点 {0} 开局分队 开局职业组 开局{key=Operator} @@ -997,6 +1003,14 @@ C:\\leidian\\LDPlayer9。\n 关卡: 普通作战 关卡: 紧急作战 关卡: 险路恶敌 + 节点: 拾遗 + 节点: 传说 + 节点: 故肆 + 节点: 易与 + 节点: 筹谋 + 节点: 常乐 + 节点: 杂疑 + 节点: 灾祸 事件: 事件名识别错误 预见: diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml index 036c2fec7e..b08bccac42 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml @@ -165,6 +165,12 @@ 燒水分隊 刷月度小隊,盡可能穩定地打更多層數 刷深入調查,盡可能穩定地打更多層數 + 刷常乐节点,第一层进洞,找不到需要的节点就重开 + 目标常乐节点 + 令 - 掷地有声 + 黍 - 种因得果 + 年 - 三缺一 + 发现目标常乐节点 {0} 開局分隊 開局職業組 開局{key=Operator} @@ -995,6 +1001,14 @@ C:\\leidian\\LDPlayer9。\n 關卡: 普通作戰 關卡: 緊急作戰 關卡: 險路惡敵 + 節點: 拾遺 + 節點: 傳說 + 節點: 故肆 + 節點: 易與 + 節點: 籌謀 + 節點: 常樂 + 節點: 雜疑 + 節點: 災禍 事件: 事件名辨識錯誤 預見: diff --git a/src/MaaWpfGui/Res/Themes/Dark.xaml b/src/MaaWpfGui/Res/Themes/Dark.xaml index aa6a174ab4..408ccd6879 100644 --- a/src/MaaWpfGui/Res/Themes/Dark.xaml +++ b/src/MaaWpfGui/Res/Themes/Dark.xaml @@ -117,4 +117,19 @@ + + + + + + + + + + + + + + + diff --git a/src/MaaWpfGui/Res/Themes/Light.xaml b/src/MaaWpfGui/Res/Themes/Light.xaml index bf93920b2b..435246541f 100644 --- a/src/MaaWpfGui/Res/Themes/Light.xaml +++ b/src/MaaWpfGui/Res/Themes/Light.xaml @@ -99,4 +99,19 @@ + + + + + + + + + + + + + + + diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs index b465c46d99..1006684aaa 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs @@ -28,9 +28,11 @@ using MaaWpfGui.ViewModels.UI; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Mode = MaaWpfGui.Configuration.Single.MaaTask.RoguelikeMode; +using RoguelikeBoskySubNodeType = MaaWpfGui.Configuration.Single.MaaTask.RoguelikeBoskySubNodeType; using Theme = MaaWpfGui.Configuration.Single.MaaTask.RoguelikeTheme; namespace MaaWpfGui.ViewModels.UserControl.TaskQueue; + public class RoguelikeSettingsUserControlModel : TaskViewModel { static RoguelikeSettingsUserControlModel() @@ -111,6 +113,7 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel new() { Display = LocalizationHelper.GetString("RoguelikeStrategyExp"), Value = Mode.Exp }, new() { Display = LocalizationHelper.GetString("RoguelikeStrategyGold"), Value = Mode.Investment }, new() { Display = LocalizationHelper.GetString("RoguelikeStrategyLastReward"), Value = Mode.Collectible }, + new() { Display = LocalizationHelper.GetString("RoguelikeStrategyFindPlaytime"), Value = Mode.FindPlaytime }, ]; break; @@ -918,6 +921,36 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel } } + private RoguelikeBoskySubNodeType _roguelikeFindPlaytimeTarget = ConfigurationHelper.GetValue(ConfigurationKeys.RoguelikeFindPlaytimeTarget, RoguelikeBoskySubNodeType.Ling); + + /// + /// Gets or sets the target playtime subnode type for FindPlaytime mode. + /// + public RoguelikeBoskySubNodeType RoguelikeFindPlaytimeTarget + { + get => _roguelikeFindPlaytimeTarget; + set + { + SetAndNotify(ref _roguelikeFindPlaytimeTarget, value); + ConfigurationHelper.SetValue(ConfigurationKeys.RoguelikeFindPlaytimeTarget, value.ToString()); + } + } + + /// + /// Gets the list of available playtime target options for FindPlaytime mode. + /// + public ObservableCollection> RoguelikeFindPlaytimeTargetList { get; } = new() + { + new() { Display = LocalizationHelper.GetString("RoguelikePlaytimeLing"), Value = RoguelikeBoskySubNodeType.Ling }, + new() { Display = LocalizationHelper.GetString("RoguelikePlaytimeShu"), Value = RoguelikeBoskySubNodeType.Shu }, + new() { Display = LocalizationHelper.GetString("RoguelikePlaytimeNian"), Value = RoguelikeBoskySubNodeType.Nian }, + }; + + /// + /// Gets a value indicating whether the FindPlaytime target selection should be visible. + /// + public bool RoguelikeFindPlaytimeTargetVisible => RoguelikeMode == Mode.FindPlaytime && RoguelikeTheme == Theme.JieGarden; + private bool _roguelikeStopAtMaxLevel = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.RoguelikeStopAtMaxLevel, bool.FalseString)); /// @@ -1057,10 +1090,44 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeEvent") + $" {subTaskDetails!["name"]}", UiLogColor.EventIS); break; + case "BoskyPassageNode": + { + var nodeType = subTaskDetails!["node_type"]?.ToString(); + var (localizedNodeType, logColor) = nodeType switch + { + "Omissions" => (LocalizationHelper.GetString("BoskyOmissions"), UiLogColor.BoskyOmissionsIS), + "Legend" => (LocalizationHelper.GetString("BoskyLegend"), UiLogColor.BoskyLegendIS), + "OldShop" => (LocalizationHelper.GetString("BoskyOldShop"), UiLogColor.BoskyOldShopIS), + "YiTrader" => (LocalizationHelper.GetString("BoskyYiTrader"), UiLogColor.TraderIS), + "Scheme" => (LocalizationHelper.GetString("BoskyScheme"), UiLogColor.BoskySchemeIS), + "Playtime" => (LocalizationHelper.GetString("BoskyPlaytime"), UiLogColor.BoskyPlaytimeIS), + "Doubts" => (LocalizationHelper.GetString("BoskyDoubts"), UiLogColor.BoskyDoubtsIS), + "Disaster" => (LocalizationHelper.GetString("BoskyDisaster"), UiLogColor.EmergencyIS), + _ => (nodeType ?? "Unknown", UiLogColor.EventIS), + }; + + Instances.TaskQueueViewModel.AddLog(localizedNodeType, logColor); + break; + } + case "EncounterOcrError": Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("EncounterOcrError"), UiLogColor.Error); break; + case "RoguelikeJieGardenTargetFound": + { + var targetSubtype = subTaskDetails!["target_subtype"]?.ToString(); + var localizedTarget = targetSubtype switch + { + "Ling" => LocalizationHelper.GetString("RoguelikePlaytimeLing"), + "Shu" => LocalizationHelper.GetString("RoguelikePlaytimeShu"), + "Nian" => LocalizationHelper.GetString("RoguelikePlaytimeNian"), + _ => targetSubtype ?? "Unknown", + }; + Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RoguelikeJieGardenTargetFound"), localizedTarget), UiLogColor.Success); + break; + } + case "FoldartalGainOcrNextLevel": Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FoldartalGainOcrNextLevel") + $" {subTaskDetails!["foldartal"]}"); break; @@ -1139,6 +1206,9 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel // 深入探索 DeepExplorationAutoIterate = RoguelikeDeepExplorationAutoIterate, + // 刷常乐节点 + FindPlaytimeTarget = RoguelikeFindPlaytimeTarget, + SamiFirstFloorFoldartal = RoguelikeTheme == Theme.Sami && RoguelikeMode == Mode.Collectible && Roguelike3FirstFloorFoldartal, SamiStartFloorFoldartal = Roguelike3FirstFloorFoldartals, SamiNewSquad2StartingFoldartal = Roguelike3NewSquad2StartingFoldartal && RoguelikeSquadIsFoldartal, diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml b/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml index 9f25f21069..8f388f3ca6 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml @@ -80,6 +80,17 @@ ItemsSource="{Binding RoguelikeRolesList}" SelectedValue="{Binding RoguelikeRoles}" SelectedValuePath="Value" /> +