diff --git a/resource/roguelike/Sarkaz/map.json b/resource/roguelike/Sarkaz/map.json new file mode 100644 index 0000000000..83c1c963de --- /dev/null +++ b/resource/roguelike/Sarkaz/map.json @@ -0,0 +1,21 @@ +{ + "theme": "Sarkaz", + "node": [ + { + "type": "CombatOps", + "template": ["Sarkaz@Roguelike@MapNodeCombatOps.png", "Sarkaz@Roguelike@MapNodeCombatOpsGrey.png"] + }, + { + "type": "EmergencyOps", + "template": ["Sarkaz@Roguelike@MapNodeEmergencyOps.png", "Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png"] + }, + { + "type": "Encounter", + "template": ["Sarkaz@Roguelike@MapNodeEncounter.png", "Sarkaz@Roguelike@MapNodeEncounterGrey.png"] + }, + { + "type": "RogueTrader", + "template": ["Sarkaz@Roguelike@MapNodeRogueTraderGrey.png"] + } + ] +} diff --git a/resource/tasks.json b/resource/tasks.json index 6f5d23da7c..3678c30114 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -6982,6 +6982,9 @@ "Roguelike@StrategyChange_mode5": { "baseTask": "Roguelike@StrategyChange_default" }, + "Roguelike@StrategyChange_mode10001": { + "baseTask": "Roguelike@StrategyChange_default" + }, "Roguelike@TempRecruitFlag": { "roi": [587, 76, 100, 20], "templThreshold": 0.5 @@ -8979,6 +8982,80 @@ "Sami@RoguelikeSettlementConfirm": { "template": "RoguelikeSettlementConfirm.png" }, + "Sarkaz@Roguelike@RoutingConfig": { + "doc": [ + "————————————————————————————————————————————————————————————————", + "RoguelikeRoutingTaskPlugin用参数", + "————————————————————————————————————————————————————————————————", + "(roi.x, roi.y): 生成地图时向左滑动起点", + "(roi.width, roi.height): 生成地图时向左滑动终点", + "specialParams[0]: 第一列节点的默认横坐标 (Rect.x)", + "specialParams[1]: 中间列节点的默认横坐标 (Rect.x)", + "specialParams[2]: 最后列节点的默认横坐标 (Rect.x)", + "specialParams[3]: 节点 Rect.width", + "specialParams[4]: 节点 Rect.height", + "specialParams[5]: 两列节点之间的距离", + "specialParams[6]: 节点 Rect 下边缘到节点铭牌下边缘的距离", + "specialParams[7]: roi 时的 margin offset", + "specialParams[8]: 节点间连线方向判定的阈值" + ], + "template": "empty.png", + "roi": [1000, 360, 570, 360], + "specialParams": [321, 341, 586, 138, 47, 450, 32, 15, 7] + }, + "Sarkaz@Roguelike@RoutingNodeAnalyze": { + "template": [ + "Sarkaz@Roguelike@MapNodeCombatOps.png", + "Sarkaz@Roguelike@MapNodeCombatOpsGrey.png", + "Sarkaz@Roguelike@MapNodeEmergencyOps.png", + "Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png", + "Sarkaz@Roguelike@MapNodeEncounter.png", + "Sarkaz@Roguelike@MapNodeEncounterGrey.png", + "Sarkaz@Roguelike@MapNodeRogueTraderGrey.png" + ], + "roi": [246, 60, 288, 575], + "postDelay": 300 + }, + "Sarkaz@Roguelike@RoutingExitThenContinue": { + "template": "Roguelike@ExitThenAbandon.png", + "roi": [18, 8, 53, 53], + "action": "ClickSelf", + "next": ["Sarkaz@Roguelike@RoutingContinue"] + }, + "Sarkaz@Roguelike@RoutingContinue": { + "template": "Sarkaz@Roguelike@Continue.png", + "roi": [1050, 550, 210, 100], + "action": "ClickSelf", + "next": ["Sarkaz@Roguelike@RoutingContinueConfirm"] + }, + "Sarkaz@Roguelike@RoutingContinueConfirm": { + "Doc": "next 里的 RoutingNodeAnalyze 是为了确保回到地图", + "algorithm": "JustReturn", + "action": "ClickRect", + "specificRect": [50, 360, 0, 0], + "next": ["Sarkaz@Roguelike@RoutingNodeAnalyze", "Sarkaz@Roguelike@RoutingContinueConfirm"] + }, + "Sarkaz@Roguelike@RoutingRefreshNode": { + "roi": [796, 150, 50, 150], + "action": "ClickSelf", + "next": ["Sarkaz@Roguelike@RoutingRefreshNodeConfirm"] + }, + "Sarkaz@Roguelike@RoutingRefreshNodeConfirm": { + "algorithm": "OcrDetect", + "roi": [993, 472, 53, 26], + "text": ["确定"], + "action": "ClickSelf", + "postDelay": 300, + "next": ["Sarkaz@Roguelike@RoutingRefreshNodeWaiting", "Sarkaz@Roguelike@RoutingRefreshNodeCompleted"] + }, + "Sarkaz@Roguelike@RoutingRefreshNodeWaiting": { + "baseTask": "LoadingText", + "postDelay": 300, + "next": ["Sarkaz@Roguelike@RoutingRefreshNodeWaiting", "Sarkaz@Roguelike@RoutingRefreshNodeCompleted"] + }, + "Sarkaz@Roguelike@RoutingRefreshNodeCompleted": { + "algorithm": "JustReturn" + }, "Sarkaz@Roguelike@Abandon": { "crop_doc": { "roi": [1067, 255, 159, 154], @@ -9907,6 +9984,55 @@ "Sarkaz@Roguelike@StageDreadfulFoe-5" ] }, + "Sarkaz@Roguelike@Stages_routing": { + "algorithm": "JustReturn", + "next": [ + "Sarkaz@Roguelike@NextLevel", + "Sarkaz@Roguelike@CloseCollectionContinue", + "Sarkaz@Roguelike@CloseCollectionClose", + "Sarkaz@Roguelike@ChooseBurdenOperatorAtStart", + "Sarkaz@Roguelike@DecreaseBurdenChaos", + "Sarkaz@Roguelike@DecreaseBurdenRetardation", + "Sarkaz@Roguelike@Routing" + ] + }, + "Sarkaz@Roguelike@Routing": { + "template": [ + "Sarkaz@Roguelike@StageCombatDps", + "Sarkaz@Roguelike@StageEmergencyDps", + "Sarkaz@Roguelike@StageDreadfulFoe", + "Sarkaz@Roguelike@StageDreadfulFoe-5", + "Sarkaz@Roguelike@StageEncounter", + "Sarkaz@Roguelike@StageBoons", + "Sarkaz@Roguelike@StageSafeHouse", + "Sarkaz@Roguelike@StageSafeHouseCR", + "Sarkaz@Roguelike@StageSafeHouseHD", + "Sarkaz@Roguelike@StageGambling", + "Sarkaz@Roguelike@StageTrader", + "Sarkaz@Roguelike@StageTraderCR", + "Sarkaz@Roguelike@StageTraderHD", + "Sarkaz@Roguelike@StageWindAndRain", + "Sarkaz@Roguelike@StageEmergencyTransportation", + "Sarkaz@Roguelike@StageBoskyPassage", + "Sarkaz@Roguelike@StageProphecy", + "Sarkaz@Roguelike@StageFilterTruth", + "Sarkaz@Roguelike@StageFilterTruthCR", + "Sarkaz@Roguelike@StageFilterTruthHD", + "Sarkaz@Roguelike@StageConfrontation" + ], + "next": ["Sarkaz@Roguelike@RoutingAction"] + }, + "Sarkaz@Roguelike@RoutingAction": { + "algorithm": "JustReturn" + }, + "Sarkaz@Roguelike@RoutingActionEncounter": { + "algorithm": "JustReturn", + "next": ["Sarkaz@Roguelike@StageEncounterEnter"] + }, + "Sarkaz@Roguelike@RoutingActionRogueTrader": { + "algorithm": "JustReturn", + "next": ["Sarkaz@Roguelike@StageTraderEnter"] + }, "Sarkaz@Roguelike@StartExplore": { "roi": [1014, 481, 266, 219] }, @@ -9951,6 +10077,18 @@ ["诡谲断章", "_exit"] ] }, + "Sarkaz@Roguelike@StrategyChange_mode10001": { + "text": ["_routing", "_exit"], + "ocrReplace": [ + ["熔魂之始", "_routing"], + ["锻铁根须", "_exit"], + ["灰铸迷城", "_exit"], + ["或然歧域", "_exit"], + ["虚实疆界", "_exit"], + ["辉光天顶", "_exit"], + ["诡谲断章", "_exit"] + ] + }, "Sarkaz@Roguelike@TodoEnter": {}, "Sarkaz@Roguelike@WaitForStartButtonClicked": { "template": "Roguelike@QuickFormation.png" diff --git a/resource/template/Sarkaz@Roguelike@MapNodeCombatOps.png b/resource/template/Sarkaz@Roguelike@MapNodeCombatOps.png new file mode 100644 index 0000000000..1db0c6215a Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeCombatOps.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeCombatOpsGrey.png b/resource/template/Sarkaz@Roguelike@MapNodeCombatOpsGrey.png new file mode 100644 index 0000000000..9688befa34 Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeCombatOpsGrey.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png new file mode 100644 index 0000000000..56588cdc58 Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png new file mode 100644 index 0000000000..8ecef13a9d Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEncounter.png b/resource/template/Sarkaz@Roguelike@MapNodeEncounter.png new file mode 100644 index 0000000000..9046c1daf1 Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeEncounter.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEncounterGrey.png b/resource/template/Sarkaz@Roguelike@MapNodeEncounterGrey.png new file mode 100644 index 0000000000..1479a8407e Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeEncounterGrey.png differ diff --git a/resource/template/Sarkaz@Roguelike@MapNodeRogueTraderGrey.png b/resource/template/Sarkaz@Roguelike@MapNodeRogueTraderGrey.png new file mode 100644 index 0000000000..c916512832 Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@MapNodeRogueTraderGrey.png differ diff --git a/resource/template/Sarkaz@Roguelike@RoutingRefreshNode.png b/resource/template/Sarkaz@Roguelike@RoutingRefreshNode.png new file mode 100644 index 0000000000..8fb7c7732f Binary files /dev/null and b/resource/template/Sarkaz@Roguelike@RoutingRefreshNode.png differ diff --git a/src/MaaCore/Config/ResourceLoader.cpp b/src/MaaCore/Config/ResourceLoader.cpp index 06b7b73019..589e43c618 100644 --- a/src/MaaCore/Config/ResourceLoader.cpp +++ b/src/MaaCore/Config/ResourceLoader.cpp @@ -16,6 +16,7 @@ #include "Miscellaneous/TilePack.h" #include "OnnxSessions.h" #include "Roguelike/RoguelikeCopilotConfig.h" +#include "Roguelike/RoguelikeMapConfig.h" #include "Roguelike/RoguelikeRecruitConfig.h" #include "Roguelike/RoguelikeShoppingConfig.h" #include "Roguelike/RoguelikeStageEncounterConfig.h" @@ -230,6 +231,11 @@ bool asst::ResourceLoader::load(const std::filesystem::path& path) RoguelikeStageEncounterConfig, "roguelike"_p / "Sami"_p / "encounter"_p / "collapse.json"_p); + // –––––––– Roguelike Map Config ––––––––––––––––––––––––––––––––––––––––––––------ + LoadResourceAndCheckRet( + RoguelikeMapConfig, + "roguelike"_p / "Sarkaz"_p / "map.json"_p); + // –––––––– Sami Plugin Config –––––––––––––––––––––––––––––––––––––––––––––––––––– LoadResourceAndCheckRet( RoguelikeFoldartalConfig, diff --git a/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.cpp b/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.cpp new file mode 100644 index 0000000000..d41cf43782 --- /dev/null +++ b/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.cpp @@ -0,0 +1,47 @@ +#include "RoguelikeMapConfig.h" + +#include + +#include "Utils/Logger.hpp" + +bool asst::RoguelikeMapConfig::parse(const json::value& json) +{ + LogTraceFunction; + + 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 } + }; + + 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()); + 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); + } + } + return true; +} + +void asst::RoguelikeMapConfig::clear() +{ + m_templ_type_mappings.clear(); +} diff --git a/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.h b/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.h new file mode 100644 index 0000000000..503b5db7dd --- /dev/null +++ b/src/MaaCore/Config/Roguelike/RoguelikeMapConfig.h @@ -0,0 +1,28 @@ +#pragma once + +#include "Config/AbstractConfig.h" +#include "Task/Roguelike/Map/RoguelikeMap.h" + +namespace asst +{ +class RoguelikeMapConfig final : public SingletonHolder, public AbstractConfig +{ +public: + virtual ~RoguelikeMapConfig() override = default; + + const RoguelikeNodeType& templ2type(const std::string& theme, const std::string& templ_name) const noexcept + { + const auto& templ_type_mapping = m_templ_type_mappings.at(theme); + return templ_type_mapping.at(templ_name); + } + +private: + virtual bool parse(const json::value& json) override; + + void clear(); + + std::unordered_map> m_templ_type_mappings; +}; + +inline static auto& RoguelikeMapInfo = RoguelikeMapConfig::get_instance(); +} diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 700cab34b2..089405dc33 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -103,6 +103,8 @@ + + @@ -130,6 +132,7 @@ + @@ -153,6 +156,7 @@ + @@ -281,6 +285,8 @@ + + @@ -311,6 +317,7 @@ + @@ -332,6 +339,7 @@ + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index 5406509af4..1ca52acd90 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -225,6 +225,9 @@ Source\Resource\Roguelike + + Source\Resource\Roguelike + Source\Resource\Roguelike @@ -456,6 +459,12 @@ Source\Task\Roguelike + + Source\Task\Roguelike + + + Source\Task\Roguelike + Source\Task\Roguelike @@ -639,6 +648,9 @@ Source\Vision\Miscellaneous + + Source\Vision\Miscellaneous + Source\Vision\Config @@ -791,6 +803,9 @@ Source\Resource\Roguelike + + Source\Resource\Roguelike + Source\Resource\Roguelike @@ -968,6 +983,12 @@ Source\Task\Roguelike + + Source\Task\Roguelike + + + Source\Task\Roguelike + Source\Task\Roguelike @@ -1127,6 +1148,9 @@ Source\Vision\Miscellaneous + + Source\Vision\Miscellaneous + Source\Vision\Config diff --git a/src/MaaCore/Task/Interface/RoguelikeTask.cpp b/src/MaaCore/Task/Interface/RoguelikeTask.cpp index 066d8ff2b1..9e4e312a05 100644 --- a/src/MaaCore/Task/Interface/RoguelikeTask.cpp +++ b/src/MaaCore/Task/Interface/RoguelikeTask.cpp @@ -31,6 +31,9 @@ #include "Task/Roguelike/Sami/RoguelikeFoldartalStartTaskPlugin.h" #include "Task/Roguelike/Sami/RoguelikeFoldartalUseTaskPlugin.h" +// –––––––– 萨卡兹主题专用配置及插件 ––––––––––––––––––– +#include "Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h" + #include "Utils/Logger.hpp" asst::RoguelikeTask::RoguelikeTask(const AsstCallback& callback, Assistant* inst) : @@ -82,6 +85,10 @@ asst::RoguelikeTask::RoguelikeTask(const AsstCallback& callback, Assistant* inst m_roguelike_task_ptr->register_plugin(m_config_ptr, m_control_ptr); m_roguelike_task_ptr->register_plugin(m_config_ptr, m_control_ptr); + // ------------------ 萨卡兹主题专用插件 ------------------ + + m_roguelike_task_ptr->register_plugin(m_config_ptr, m_control_ptr); + // 这个任务如果卡住会放弃当前的肉鸽并重新开始,所以多添加亿点。先这样凑合用 for (int i = 0; i != 999; ++i) { m_subtasks.emplace_back(m_roguelike_task_ptr); diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.cpp b/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.cpp new file mode 100644 index 0000000000..f0fead458c --- /dev/null +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.cpp @@ -0,0 +1,271 @@ +#include "RoguelikeMap.h" + +#include +#include + +#include "Utils/Logger.hpp" +#include "Utils/Ranges.hpp" + +asst::RoguelikeMap::RoguelikeMap() +{ + m_curr_pos = create_and_insert_node(RoguelikeNodeType::Init, init_index, 0).value(); +} + +// ———————————————————————————————————————————————————————————————————————————————— +// update map +// ———————————————————————————————————————————————————————————————————————————————— + +std::optional + asst::RoguelikeMap::create_and_insert_node(const RoguelikeNodeType& type, const size_t& column, const int& y) +{ + const RoguelikeNodePtr node = std::make_shared(type, column, y); + return insert_node(node, column); +} + +void asst::RoguelikeMap::add_edge(const size_t& source, const size_t& target) +{ + if (source >= m_nodes.size() || target >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + return; + } + + const RoguelikeNodePtr& source_node = m_nodes.at(source); + const RoguelikeNodePtr& target_node = m_nodes.at(target); + source_node->succs.emplace_back(target); + target_node->preds.emplace_back(source); + Log.info(__FUNCTION__, "| Node", source, "-> Node", target); +} + +void asst::RoguelikeMap::set_curr_pos(const size_t& node_index) +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + return; + } + + Log.info(__FUNCTION__, "| move from Node", m_curr_pos, "to Node ", node_index); + m_curr_pos = node_index; +} + +void asst::RoguelikeMap::update_node_costs() +{ + for (const RoguelikeNodePtr& node : ranges::reverse_view(m_nodes)) { + node->cost = m_cost_fun(node); + if (!node->succs.empty()) { + auto succ_costs = node->succs | views::transform([&](const size_t node_index) { + return m_cost_fun(m_nodes.at(node_index)); + }); + node->cost += ranges::min(succ_costs); + } + } +} + +void asst::RoguelikeMap::reset() +{ + m_nodes.clear(); + m_column_indices.clear(); + m_curr_pos = create_and_insert_node(RoguelikeNodeType::Init, init_index, 0).value(); +} + +// ———————————————————————————————————————————————————————————————————————————————— +// get map information +// ———————————————————————————————————————————————————————————————————————————————— + +size_t asst::RoguelikeMap::get_column_begin(const size_t& column) const +{ + if (column == init_index) { + return init_index; + } + if (column >= m_column_indices.size()) { + Log.warn(__FUNCTION__, "| column does not exist"); + return m_nodes.size(); + } + return m_column_indices.at(column - 1); +} + +size_t asst::RoguelikeMap::get_column_end(const size_t& column) const +{ + if (column >= m_column_indices.size()) { + Log.warn(__FUNCTION__, "| column does not exist"); + return m_nodes.size(); + } + return m_column_indices.at(column); +} + +size_t asst::RoguelikeMap::get_next_node() const +{ + const RoguelikeNodePtr curr = m_nodes.at(m_curr_pos); + + if (curr->succs.empty()) { + Log.error(__FUNCTION__, "| no successor nodes"); + return m_curr_pos; + } + + const size_t next_index = + ranges::min(curr->succs, [&](const size_t& node1_index, const size_t& node2_index) { + return m_nodes.at(node1_index)->cost < m_nodes.at(node2_index)->cost; + }); + return next_index; +} + +// ———————————————————————————————————————————————————————————————————————————————— +// get node fields +// ———————————————————————————————————————————————————————————————————————————————— + +/* +#define DEFINE_GET_NODE_FIELD(RETURN_TYPE, FIELD) \ +RETURN_TYPE asst::RoguelikeMap::get_node_##FIELD(const size_t& node_index) const \ +{ \ + if (node_index >= m_nodes.size()) { \ + Log.error(__FUNCTION__, "| node does not exist"); \ + } \ + \ + return m_nodes.at(node_index)->FIELD; \ +} + +DEFINE_GET_NODE_FIELD(asst::RoguelikeNodeType, type) +DEFINE_GET_NODE_FIELD(size_t, column) +DEFINE_GET_NODE_FIELD(int, y) +DEFINE_GET_NODE_FIELD(bool, visited) +DEFINE_GET_NODE_FIELD(std::vector, succs) +DEFINE_GET_NODE_FIELD(std::vector, preds) +DEFINE_GET_NODE_FIELD(int, cost) +DEFINE_GET_NODE_FIELD(int, refresh_times) + +#undef DEFINE_GET_NODE_FIELD +*/ + +asst::RoguelikeNodeType asst::RoguelikeMap::get_node_type(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->type; +} + +size_t asst::RoguelikeMap::get_node_column(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->column; +} + +int asst::RoguelikeMap::get_node_y(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->y; +} + +bool asst::RoguelikeMap::get_node_visited(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->visited; +} + +std::vector asst::RoguelikeMap::get_node_succs(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->succs; +} + +std::vector asst::RoguelikeMap::get_node_preds(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->preds; +} + +int asst::RoguelikeMap::get_node_cost(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->cost; +} + +int asst::RoguelikeMap::get_node_refresh_times(const size_t& node_index) const +{ + if (node_index >= m_nodes.size()) { + Log.error(__FUNCTION__, "| node does not exist"); + } + + return m_nodes.at(node_index)->refresh_times; +} + +// ———————————————————————————————————————————————————————————————————————————————— +// set node fields +// ———————————————————————————————————————————————————————————————————————————————— + +void asst::RoguelikeMap::set_node_type(const size_t& node_index, RoguelikeNodeType type) +{ + m_nodes.at(node_index)->type = type; +} + +void asst::RoguelikeMap::set_node_visited(const size_t& node_index, bool visisted) +{ + m_nodes.at(node_index)->visited = visisted; +} + +void asst::RoguelikeMap::set_node_refresh_times(const size_t& node_index, int refresh_times) +{ + m_nodes.at(node_index)->refresh_times = refresh_times; +} + +// ================================================================================ +// private +// ================================================================================ + +std::optional asst::RoguelikeMap::insert_node(const RoguelikeNodePtr& node, const size_t& column) +{ + // 第一个 node 必须为 init node + if (column != init_index && m_nodes.empty()) [[unlikely]] { + Log.error(__FUNCTION__, "| insert node to column", column, "before init node"); + return std::nullopt; + } + // 只允许有一个 init node + if (column == init_index && column < m_column_indices.size() && m_column_indices.at(column) > 0) + [[unlikely]] { + Log.error(__FUNCTION__, "| init node has already exist"); + return std::nullopt; + } + // 边界鉴定 + if (column > std::numeric_limits::max()) [[unlikely]] { // to avoid narrowing conversion + Log.error(__FUNCTION__, "| column index", column, "is out of boundary"); + return std::nullopt; + } + + // 在现有列外插入 node,为其创建新列 + if (column >= m_column_indices.size()) { + m_column_indices.insert(m_column_indices.end(), column - m_column_indices.size() + 1, m_nodes.size()); + } + + const size_t index = m_column_indices.at(column); + + // 边界鉴定 + if (index > std::numeric_limits::max()) [[unlikely]] { // to avoid narrowing conversion + Log.error(__FUNCTION__, "| node index", index, "is out of boundary"); + return std::nullopt; + } + + m_nodes.insert(m_nodes.begin() + static_cast(index), node); + std::for_each(m_column_indices.begin() + static_cast(column), m_column_indices.end(), [](size_t& x) { + x += 1; + }); + + return index; +} diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.h b/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.h new file mode 100644 index 0000000000..354528fee2 --- /dev/null +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeMap.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include + +namespace asst +{ +enum class RoguelikeNodeType +{ + Init = -1, // Init + Unknown = 0, // 未知 + CombatOps = 1, // 作战 + EmergencyOps = 2, // 紧急作战 + DreadfulFoe = 3, // 险路恶敌 + Encounter = 4, // 不期而遇 + Boons = 5, // 古堡馈赠/得偿所愿 + SafeHouse = 6, // 安全的角落 + Recreation = 7, // 幕间余兴/兴致盎然 + RogueTrader = 8, // 诡意行商 + // –––––––– 水月主题引入 –––––––––––––––––––––––– + RegionalCommissions = 9, // 地区委托 + LostAndFound = 10, // 风雨际会/失与得 + Scout = 11, // 紧急运输/先行一步 + BoskyPassage = 12, // 误入奇境/树篱之途 + // –––––––– 萨米主题引入 –––––––––––––––––––––––– + Prophecy = 13, // 命运所指 + MysteriousPresage = 14, // 诡秘的预感 + FerociousPresage = 15, // 凶戾的预感 + // –––––––– 萨卡兹主题引入 –––––––––––––––––––––––– + IdeaFilter = 16, // 去违存真 + FaceOff = 17 // 狭路相逢 +}; + +struct RoguelikeNode +{ + RoguelikeNodeType type = RoguelikeNodeType::Unknown; + const size_t column = 0; + const int y = 0; + bool visited = false; + std::vector succs; // successors 在 RoguelikeMap().m_nodes 中的 index + std::vector preds; // predecessors 在 RoguelikeMap().m_nodes 中的 index + int cost = 0; + // –––––––– 萨卡兹主题引入 –––––––––––––––––––––––– + int refresh_times = 0; + + RoguelikeNode(const RoguelikeNodeType _t, const size_t _c, const int _y) : + type(_t), + column(_c), + y(_y) + { + } +}; + +using RoguelikeNodePtr = std::shared_ptr; +using RoguelikeNodeCostFun = std::function; + +// ================================================================================ +// Q: RoguelikeMap 应该支持哪些功能? +// A: 原则上我希望 RoguelikeNode 和 RoguelikeNodePtr 仅在RoguelikeMap 内部调用 +// 外部操作对节点的识别以 index 为准, 因此 public 成员函数往往应当接受 size_t 类型 +// 的 node_index 参数 +// ================================================================================ +class RoguelikeMap +{ +public: + RoguelikeMap(); + + // ———————— update map ———————————————————————————————————————————————————————————— + // 创建并插入一个 node,返回新 node 的 index + std::optional + create_and_insert_node(const RoguelikeNodeType& type, const size_t& column, const int& y); + + void add_edge(const size_t& source, const size_t& target); + void set_curr_pos(const size_t& node_index); + void set_cost_fun(const RoguelikeNodeCostFun& cost_fun) { m_cost_fun = cost_fun; } + void update_node_costs(); + void reset(); + + // ———————— get map details ——————————————————————————————————————————————————————— + size_t size() const { return m_nodes.size(); } + size_t get_num_columns() const { return m_column_indices.size(); } + size_t get_curr_pos() const { return m_curr_pos; } + size_t get_column_begin(const size_t& column) const; + size_t get_column_end(const size_t& column) const; + size_t get_next_node() const; + + // ———————— get node fields ——————————————————————————————————————————————————————— + RoguelikeNodeType get_node_type(const size_t& node_index) const; + size_t get_node_column(const size_t& node_index) const; + int get_node_y(const size_t& node_index) const; + bool get_node_visited(const size_t& node_index) const; + std::vector get_node_succs(const size_t& node_index) const; + std::vector get_node_preds(const size_t& node_index) const; + int get_node_cost(const size_t& node_index) const; + int get_node_refresh_times(const size_t& node_index) const; + + // ———————— set node fields ——————————————————————————————————————————————————————— + void set_node_type(const size_t& node_index, RoguelikeNodeType type); + void set_node_visited(const size_t& node_index, bool visisted); + void set_node_refresh_times(const size_t& node_index, int refresh_times); + + // ———————— constants and variables ——————————————————————————————————————————————— + const size_t init_index = 0; // 常量,既是 init 的 node index 也是它的 column index + +private: + // ———————— update map ———————————————————————————————————————————————————————————— + std::optional insert_node(const RoguelikeNodePtr& node, const size_t& column); + + // ———————— constants and variables ——————————————————————————————————————————————— + std::vector m_nodes; + std::vector m_column_indices; // m_column_indices[c] 代表列 c 的 node index 的上限 (exclusive) + size_t m_curr_pos = 0; // 当前位置的 node index + RoguelikeNodeCostFun m_cost_fun = [&]([[maybe_unused]] const RoguelikeNodePtr& node) { return 0; }; +}; +} diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp new file mode 100644 index 0000000000..af8349462e --- /dev/null +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp @@ -0,0 +1,333 @@ +#include "RoguelikeRoutingTaskPlugin.h" + +#include +#include + +#include "Config/TaskData.h" +#include "Controller/Controller.h" +#include "Task/ProcessTask.h" +#include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Vision/Matcher.h" +#include "Vision/Miscellaneous/BrightPointAnalyzer.h" +#include "Vision/MultiMatcher.h" + +bool asst::RoguelikeRoutingTaskPlugin::load_params([[maybe_unused]] const json::value& params) +{ + LogTraceFunction; + const std::string& theme = m_config->get_theme(); + + // 本插件暂处于实验阶段,仅用于萨卡兹肉鸽 + if (theme != RoguelikeTheme::Sarkaz) { + return false; + } + + const std::shared_ptr config = Task.get(theme + "@Roguelike@RoutingConfig"); + + m_swipe_source.x = config->roi.x; + m_swipe_source.y = config->roi.y; + m_swipe_target.x = config->roi.width; + m_swipe_target.y = config->roi.height; + m_origin_x = config->special_params.at(0); + m_middle_x = config->special_params.at(1); + m_last_x = config->special_params.at(2); + m_node_width = config->special_params.at(3); + m_node_height = config->special_params.at(4); + m_column_offset = config->special_params.at(5); + m_nameplate_offset = config->special_params.at(6); + m_roi_margin = config->special_params.at(7); + m_direction_threshold = config->special_params.at(8); + + return true; +} + +void asst::RoguelikeRoutingTaskPlugin::reset_in_run_variables() +{ + m_map.reset(); + m_need_generate_map = true; + m_selected_column = 0; + m_selected_x = 0; +} + +bool asst::RoguelikeRoutingTaskPlugin::verify(const AsstMsg msg, const json::value& details) const +{ + if (msg != AsstMsg::SubTaskStart || details.get("subtask", std::string()) != "ProcessTask") { + return false; + } + + const std::string& task_name = details.get("details", "task", ""); + if (task_name == m_config->get_theme() + "@Roguelike@Routing") { + return true; + } + + return false; +} + +bool asst::RoguelikeRoutingTaskPlugin::_run() +{ + LogTraceFunction; + + 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(); + + return true; +} + +void asst::RoguelikeRoutingTaskPlugin::generate_map() +{ + LogTraceFunction; + + const std::string& theme = m_config->get_theme(); + + m_map.reset(); + size_t curr_col = m_map.init_index + 1; + Rect roi = Task.get(theme + "@Roguelike@RoutingNodeAnalyze")->roi; + + // 第一列节点 + cv::Mat image = ctrler()->get_image(); + MultiMatcher node_analyzer(image); + node_analyzer.set_task_info(theme + "@Roguelike@RoutingNodeAnalyze"); + if (!node_analyzer.analyze()) { + Log.error(__FUNCTION__, "| no nodes found in the first column"); + return; + } + MultiMatcher::ResultsVec match_results = node_analyzer.get_result(); + sort_by_vertical_(match_results); // 按照垂直方向排序(从上到下) + for (const auto& [rect, score, templ_name] : match_results) { + const RoguelikeNodeType type = RoguelikeMapInfo.templ2type(theme, templ_name); + const size_t node = m_map.create_and_insert_node(type, curr_col, rect.y).value(); + generate_edges(node, image, rect.x); + } + + // 第二列及以后的节点 + roi.x += m_column_offset; + node_analyzer.set_roi(roi); + while (!need_exit() && node_analyzer.analyze()) { + ++curr_col; + match_results = node_analyzer.get_result(); + sort_by_vertical_(match_results); + for (const auto& [rect, score, templ_name] : match_results) { + const RoguelikeNodeType type = RoguelikeMapInfo.templ2type(theme, templ_name); + const size_t node = m_map.create_and_insert_node(type, curr_col, rect.y).value(); + generate_edges(node, image, rect.x); + } + ctrler()->swipe(m_swipe_source, m_swipe_target, 200, true); + sleep(200); + image = ctrler()->get_image(); + node_analyzer.set_image(image); + } + + ProcessTask(*this, { theme + "@Roguelike@RoutingExitThenContinue" }).run(); // 通过退出重进回到初始位置 + + return; +} + +void asst::RoguelikeRoutingTaskPlugin::generate_edges(const size_t& node, const cv::Mat& image, const int& node_x) +{ + LogTraceFunction; + + const size_t node_column = m_map.get_node_column(node); + + if (node_column == m_map.init_index) { + Log.error(__FUNCTION__, "| cannot generate edges for init node"); + return; + } + + if (node_column == m_map.init_index + 1) { + m_map.add_edge(m_map.init_index, node); // 第一列节点直接与 init 连接 + return; + } + + // 将 image转换为二值图像后计算亮点 + BrightPointAnalyzer analyzer(image); + + const int center_x = node_x - (m_column_offset - m_node_width) / 2; // node 与 前一列节点的中点横坐标 + const int node_y = m_map.get_node_y(node); + Rect roi(0, 0, m_roi_margin * 2, m_roi_margin * 2); + + // 遍历前一列节点 + const size_t pre_col_begin = m_map.get_column_begin(node_column - 1); + const size_t pre_col_end = m_map.get_column_end(node_column - 1); + for (size_t prev = pre_col_begin; prev < pre_col_end; ++prev) { + const int prev_y = m_map.get_node_y(prev); + const int center_y = (prev_y + node_y + m_node_height) / 2; + roi.x = center_x - m_roi_margin; + roi.y = center_y - m_roi_margin; + analyzer.set_roi(roi); + + if (!analyzer.analyze()) { // 节点间没有连线 + continue; + } + + // 按照水平方向排序(从左到右) + std::vector brightPoints = analyzer.get_result(); + + auto [x_min_p, x_max_p] = ranges::minmax(brightPoints, /*comp=*/ {}, [](const Point& p) { return p.x; }); + const int leftmost_x = x_min_p.x; + const int rightmost_x = x_max_p.x; + + auto leftmostBrightPoints = brightPoints | views::filter([&](const Point& p) { return p.x == leftmost_x; }); + auto rightmostBrightPoints = brightPoints | views::filter([&](const Point& p) { return p.x == rightmost_x; }); + + auto [leftmost_y_min_p, leftmost_y_max_p] = + ranges::minmax(leftmostBrightPoints, /*comp=*/ {}, [](const Point& p) { return p.y; }); + const int leftmost_y = (leftmost_y_min_p.y + leftmost_y_max_p.y) / 2; + + auto [rightmost_y_min_p, rightmost_y_max_p] = + ranges::minmax(rightmostBrightPoints, /*comp=*/ {}, [](const Point& p) { return p.y; }); + const int rightmost_y = (rightmost_y_min_p.y + rightmost_y_max_p.y) / 2; + + if (std::abs(prev_y - node_y) < m_direction_threshold && + std::abs(leftmost_y - rightmost_y) < m_direction_threshold) { + m_map.add_edge(prev, node); + } + else if (prev_y < node_y && leftmost_y < rightmost_y - m_direction_threshold) { + m_map.add_edge(prev, node); + } + else if (prev_y > node_y && leftmost_y > rightmost_y + m_direction_threshold) { + m_map.add_edge(prev, node); + } + } + + // 同列前一个节点 + if (node > m_map.get_column_begin(node_column)) { + size_t prev = node - 1; + roi.x = node_x + m_node_width / 2 - m_roi_margin; + roi.y = (m_map.get_node_y(prev) + m_node_height + m_nameplate_offset + node_y) / 2 - m_roi_margin; + analyzer.set_roi(roi); + if (analyzer.analyze()) { + m_map.add_edge(prev, node); + m_map.add_edge(node, prev); + } + } +} + +void asst::RoguelikeRoutingTaskPlugin::refresh_following_combat_nodes() +{ + LogTraceFunction; + + const std::string& theme = m_config->get_theme(); + + const size_t curr_node = m_map.get_curr_pos(); + const size_t curr_node_column = m_map.get_node_column(curr_node); + + for (size_t next_node : m_map.get_node_succs(curr_node)) { + // 不刷新同一列的节点 + const size_t next_node_column = m_map.get_node_column(next_node); + if (next_node_column <= curr_node_column) { + continue; + } + // 每个节点仅刷新一次 + if (m_map.get_node_refresh_times(next_node)) { + continue; + } + // 不刷新非战斗节点 + RoguelikeNodeType next_node_type = m_map.get_node_type(next_node); + if (next_node_type != RoguelikeNodeType::CombatOps && next_node_type != RoguelikeNodeType::EmergencyOps && + next_node_type != RoguelikeNodeType::DreadfulFoe) { + continue; + } + + int next_node_x = m_selected_x + (next_node_column == m_selected_column ? 0 : m_column_offset); + int next_node_y = m_map.get_node_y(next_node); + Rect next_node_rect = Rect(next_node_x, next_node_y, m_node_width, m_node_height); + + // 点击节点 + ctrler()->click(next_node_rect); + m_selected_column = m_map.get_node_column(next_node); + update_selected_x(); + next_node_rect.x = m_selected_x; + sleep(200); + + // 刷新节点 + ProcessTask(*this, { m_config->get_theme() + "@Roguelike@RoutingRefreshNode" }).run(); + m_map.set_node_refresh_times(next_node, m_map.get_node_refresh_times(next_node) + 1); + + // 识别并更新节点类型 + Matcher node_analyzer(ctrler()->get_image()); + node_analyzer.set_task_info(theme + "@Roguelike@RoutingNodeAnalyze"); + node_analyzer.set_roi(next_node_rect); + if (node_analyzer.analyze()) { + Matcher::Result match_results = node_analyzer.get_result(); + m_map.set_node_type(next_node, RoguelikeMapInfo.templ2type(theme, match_results.templ_name)); + } + } +} + +void asst::RoguelikeRoutingTaskPlugin::navigate_route() +{ + LogTraceFunction; + + const size_t curr_column = m_map.get_node_column(m_map.get_curr_pos()); + + m_map.set_cost_fun([&](const RoguelikeNodePtr& node) { + if (node->visited) { + return 1000; + } + + if (node->column == curr_column) { + return 1000; + } + + if (node->type == RoguelikeNodeType::CombatOps || node->type == RoguelikeNodeType::EmergencyOps || + node->type == RoguelikeNodeType::DreadfulFoe) { + return 1 + (node->refresh_times ? 999 : 0); + } + + return 0; + }); + + m_map.update_node_costs(); + + const size_t next_node = m_map.get_next_node(); + + if (m_map.get_node_cost(next_node) >= 1000) { + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@ExitThenAbandon"); + reset_in_run_variables(); + return; + } + + const size_t next_node_column = m_map.get_node_column(next_node); + const int next_node_x = m_selected_x + (next_node_column == m_selected_column ? 0 : m_column_offset); + 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); + + if (m_map.get_node_type(next_node) == RoguelikeNodeType::Encounter) { + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingActionEncounter"); + m_map.set_curr_pos(next_node); + } + else if (m_map.get_node_type(next_node) == RoguelikeNodeType::RogueTrader) { + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@RoutingActionRogueTrader"); + reset_in_run_variables(); + } + else { + Task.set_task_base("Sarkaz@Roguelike@RoutingAction", "Sarkaz@Roguelike@ExitThenAbandon"); + reset_in_run_variables(); + } +} + +void asst::RoguelikeRoutingTaskPlugin::update_selected_x() +{ + if (m_selected_column == m_map.init_index) { + m_selected_x = m_origin_x - m_column_offset; + } + else if (m_selected_column == m_map.init_index + 1) { + m_selected_x = m_origin_x; + } + else if (m_selected_column == m_map.get_num_columns() - 1) [[unlikely]] { + m_selected_x = m_last_x; + } + else { + m_selected_x = m_middle_x; + } +} diff --git a/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h new file mode 100644 index 0000000000..ece60a618f --- /dev/null +++ b/src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h @@ -0,0 +1,46 @@ +#pragma once + +#include "Common/AsstTypes.h" +#include "Config/Roguelike/RoguelikeMapConfig.h" +#include "Task/Roguelike/AbstractRoguelikeTaskPlugin.h" + +namespace asst +{ +class RoguelikeRoutingTaskPlugin : public AbstractRoguelikeTaskPlugin +{ +public: + using AbstractRoguelikeTaskPlugin::AbstractRoguelikeTaskPlugin; + virtual ~RoguelikeRoutingTaskPlugin() override = default; + virtual bool verify(AsstMsg msg, const json::value& details) const override; + virtual bool load_params(const json::value& params) override; + virtual void reset_in_run_variables() override; + +protected: + virtual bool _run() override; + +private: + void generate_map(); + void generate_edges(const size_t& node, const cv::Mat& image, const int& node_x); + void refresh_following_combat_nodes(); + void navigate_route(); + void update_selected_x(); + + // ———————— constants and variables ——————————————————————————————————————————————— + RoguelikeMap m_map; + bool m_need_generate_map = true; + size_t m_selected_column = 0; // 当前选中节点所在列 + int m_selected_x = 0; // 当前选中节点的横坐标 (Rect.x) + + Point m_swipe_source = { 0, 0 }; // 生成地图时向左滑动起点 + Point m_swipe_target = { 0, 0 }; // 生成地图时向左滑动终点 + int m_origin_x = 0; // 第一列节点的默认横坐标 (Rect.x) + int m_middle_x = 0; // 中间列节点的默认横坐标 (Rect.x) + int m_last_x = 0; // 最后列节点的默认横坐标 (Rect.x) + int m_node_width = 0; // 节点 Rect.width + int m_node_height = 0; // 节点 Rect.height + int m_column_offset = 0; // 两列节点之间的距离 + int m_nameplate_offset = 0; // 节点 Rect 下边缘到节点铭牌下边缘的距离 + int m_roi_margin = 0; // roi 时的 margin offset + int m_direction_threshold = 0; // 节点间连线方向判定的阈值 +}; +} diff --git a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h index 37d3a01daa..f6d3fc5f72 100644 --- a/src/MaaCore/Task/Roguelike/RoguelikeConfig.h +++ b/src/MaaCore/Task/Roguelike/RoguelikeConfig.h @@ -28,7 +28,10 @@ namespace asst Collectible = 4, // 4 - 刷开局,以获得热水壶或者演讲稿开局或只凹直升,不期而遇采用保守策略 // ------------------ 萨米主题专用模式 ------------------ - CLP_PDS = 5 // 5 - 刷隐藏坍缩范式,以增加坍缩值为最优先目标 + CLP_PDS = 5, // 5 - 刷隐藏坍缩范式,以增加坍缩值为最优先目标 + + // ------------------ 萨卡兹主题专用模式 ------------------ + FastPass = 10001 // 10001 - 快速通过第一层 }; struct RoguelikeOper @@ -52,7 +55,8 @@ namespace asst return mode == RoguelikeMode::Exp || mode == RoguelikeMode::Investment || mode == RoguelikeMode::Collectible || - (mode == RoguelikeMode::CLP_PDS && theme == RoguelikeTheme::Sami); + (mode == RoguelikeMode::CLP_PDS && theme == RoguelikeTheme::Sami) || + (mode == RoguelikeMode::FastPass && theme == RoguelikeTheme::Sarkaz); } bool verify_and_load_params(const json::value& params); diff --git a/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.cpp b/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.cpp new file mode 100644 index 0000000000..e7eb60f417 --- /dev/null +++ b/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.cpp @@ -0,0 +1,33 @@ +#include "BrightPointAnalyzer.h" + +#include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Utils/Ranges.hpp" + +using namespace asst; + +BrightPointAnalyzer::ResultsVecOpt BrightPointAnalyzer::analyze() +{ + const cv::Mat croppedImage = make_roi(m_image, m_roi); + cv::Mat grayImage; + cv::cvtColor(croppedImage, grayImage, cv::COLOR_BGR2GRAY); + cv::Mat binaryImage; + cv::threshold(grayImage, binaryImage, 200, 255, cv::THRESH_BINARY); + std::vector brightPoints; + cv::findNonZero(binaryImage, brightPoints); + + if (brightPoints.empty()) { + return std::nullopt; + } + + auto transform_view = brightPoints | views::transform([](const cv::Point& p) { return Point(p.x, p.y); }); + ResultsVec results(ranges::begin(transform_view), ranges::end(transform_view)); + + if (m_log_tracing) { + Log.trace("analyze_bright_points | num:", results.size()); + } + + // FIXME: 老接口太难重构了,先弄个这玩意兼容下,后续慢慢全删掉 + m_result = std::move(results); + return m_result; +} diff --git a/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.h b/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.h new file mode 100644 index 0000000000..d0050c71f3 --- /dev/null +++ b/src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Vision/VisionHelper.h" + +namespace asst +{ +class BrightPointAnalyzer : public VisionHelper +{ +public: + using Result = Point; + using ResultsVec = std::vector; + using ResultsVecOpt = std::optional; + + using VisionHelper::VisionHelper; + virtual ~BrightPointAnalyzer() override = default; + + ResultsVecOpt analyze(); + // FIXME: 老接口太难重构了,先弄个这玩意兼容下,后续慢慢全删掉 + const auto& get_result() const noexcept { return m_result; } + +private: + ResultsVec m_result; +}; +}