feat: 加入肉鸽节点地图识别功能,并应用于萨卡兹肉鸽蓝图快速过图 (#9812)

* feat: 添加 RoguelikeMode::FastPass,负责用蓝图分队快速过图

* chore: update templates for RoguelikeMap and RoguelikeRouting

* feat: RoguelikeMap

* feat: RoguelikeMapConfig

* feat: BrightPointAnalyzer

* feat: RoguelikeRouting

* chore: Auto update by pre-commit hooks [skip changelog]

* fix: 修复会卡在商店门口的bug

* fix: adapt new plugin-registration signature

* chore: try fix submodule

* perf: 复用task

* chore: Auto update by pre-commit hooks [skip changelog]

* fix: 为函数申明补上定义

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: status102 <102887808+status102@users.noreply.github.com>
This commit is contained in:
Weiyou Wang
2024-08-09 22:54:19 +10:00
committed by GitHub
parent 7e9b5d0006
commit c82aeb0256
23 changed files with 1109 additions and 2 deletions

View File

@@ -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"]
}
]
}

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -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,

View File

@@ -0,0 +1,47 @@
#include "RoguelikeMapConfig.h"
#include <meojson/json.hpp>
#include "Utils/Logger.hpp"
bool asst::RoguelikeMapConfig::parse(const json::value& json)
{
LogTraceFunction;
static const std::unordered_map<std::string, RoguelikeNodeType> 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();
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "Config/AbstractConfig.h"
#include "Task/Roguelike/Map/RoguelikeMap.h"
namespace asst
{
class RoguelikeMapConfig final : public SingletonHolder<RoguelikeMapConfig>, 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<std::string, std::unordered_map<std::string, RoguelikeNodeType>> m_templ_type_mappings;
};
inline static auto& RoguelikeMapInfo = RoguelikeMapConfig::get_instance();
}

View File

@@ -103,6 +103,8 @@
<ClInclude Include="Task\Roguelike\Sami\RoguelikeFoldartalStartTaskPlugin.h" />
<ClInclude Include="Task\Roguelike\Sami\RoguelikeFoldartalUseTaskPlugin.h" />
<ClInclude Include="Task\Roguelike\Sami\RoguelikeCollapsalParadigmTaskPlugin.h" />
<ClInclude Include="Task\Roguelike\Map\RoguelikeMap.h" />
<ClInclude Include="Task\Roguelike\Map\RoguelikeRoutingTaskPlugin.h" />
<ClInclude Include="Task\SSS\SSSBattleProcessTask.h" />
<ClInclude Include="Task\SSS\SSSDropRewardsTaskPlugin.h" />
<ClInclude Include="Task\SSS\SSSStageManagerTask.h" />
@@ -130,6 +132,7 @@
<ClInclude Include="Vision\Miscellaneous\RecruitImageAnalyzer.h" />
<ClInclude Include="Vision\Miscellaneous\StageDropsImageAnalyzer.h" />
<ClInclude Include="Vision\Miscellaneous\OperBoxImageAnalyzer.h" />
<ClInclude Include="Vision\Miscellaneous\BrightPointAnalyzer.h" />
<ClInclude Include="Vision\MultiMatcher.h" />
<ClInclude Include="Vision\OCRer.h" />
<ClInclude Include="Vision\TemplDetOCRer.h" />
@@ -153,6 +156,7 @@
<ClInclude Include="Config\Miscellaneous\TilePack.h" />
<ClInclude Include="Config\ResourceLoader.h" />
<ClInclude Include="Config\Roguelike\RoguelikeCopilotConfig.h" />
<ClInclude Include="Config\Roguelike\RoguelikeMapConfig.h" />
<ClInclude Include="Config\Roguelike\RoguelikeRecruitConfig.h" />
<ClInclude Include="Config\Roguelike\RoguelikeShoppingConfig.h" />
<ClInclude Include="Config\Roguelike\RoguelikeStageEncounterConfig.h" />
@@ -281,6 +285,8 @@
<ClCompile Include="Task\Roguelike\Sami\RoguelikeFoldartalStartTaskPlugin.cpp" />
<ClCompile Include="Task\Roguelike\Sami\RoguelikeFoldartalUseTaskPlugin.cpp" />
<ClCompile Include="Task\Roguelike\Sami\RoguelikeCollapsalParadigmTaskPlugin.cpp" />
<ClCompile Include="Task\Roguelike\Map\RoguelikeMap.cpp" />
<ClCompile Include="Task\Roguelike\Map\RoguelikeRoutingTaskPlugin.cpp" />
<ClCompile Include="Task\Roguelike\RoguelikeConfig.cpp" />
<ClCompile Include="Task\Roguelike\RoguelikeDifficultySelectionTaskPlugin.cpp" />
<ClCompile Include="Task\Roguelike\RoguelikeInvestTaskPlugin.cpp" />
@@ -311,6 +317,7 @@
<ClCompile Include="Vision\Miscellaneous\RecruitImageAnalyzer.cpp" />
<ClCompile Include="Vision\Miscellaneous\StageDropsImageAnalyzer.cpp" />
<ClCompile Include="Vision\Miscellaneous\OperBoxImageAnalyzer.cpp" />
<ClCompile Include="Vision\Miscellaneous\BrightPointAnalyzer.cpp" />
<ClCompile Include="Vision\MultiMatcher.cpp" />
<ClCompile Include="Vision\OCRer.cpp" />
<ClCompile Include="Vision\TemplDetOCRer.cpp" />
@@ -332,6 +339,7 @@
<ClCompile Include="Config\Miscellaneous\TilePack.cpp" />
<ClCompile Include="Config\ResourceLoader.cpp" />
<ClCompile Include="Config\Roguelike\RoguelikeCopilotConfig.cpp" />
<ClCompile Include="Config\Roguelike\RoguelikeMapConfig.cpp" />
<ClCompile Include="Config\Roguelike\RoguelikeRecruitConfig.cpp" />
<ClCompile Include="Config\Roguelike\RoguelikeShoppingConfig.cpp" />
<ClCompile Include="Config\Roguelike\RoguelikeStageEncounterConfig.cpp" />

View File

@@ -225,6 +225,9 @@
<ClInclude Include="Config\Roguelike\RoguelikeCopilotConfig.h">
<Filter>Source\Resource\Roguelike</Filter>
</ClInclude>
<ClInclude Include="Config\Roguelike\RoguelikeMapConfig.h">
<Filter>Source\Resource\Roguelike</Filter>
</ClInclude>
<ClInclude Include="Config\Roguelike\RoguelikeRecruitConfig.h">
<Filter>Source\Resource\Roguelike</Filter>
</ClInclude>
@@ -456,6 +459,12 @@
<ClInclude Include="Task\Roguelike\RoguelikeBattleTaskPlugin.h">
<Filter>Source\Task\Roguelike</Filter>
</ClInclude>
<ClInclude Include="Task\Roguelike\Map\RoguelikeMap.h">
<Filter>Source\Task\Roguelike</Filter>
</ClInclude>
<ClInclude Include="Task\Roguelike\Map\RoguelikeRoutingTask.h">
<Filter>Source\Task\Roguelike</Filter>
</ClInclude>
<ClInclude Include="Task\Roguelike\RoguelikeConfig.h">
<Filter>Source\Task\Roguelike</Filter>
</ClInclude>
@@ -639,6 +648,9 @@
<ClInclude Include="Vision\Miscellaneous\OperBoxImageAnalyzer.h">
<Filter>Source\Vision\Miscellaneous</Filter>
</ClInclude>
<ClInclude Include="Vision\Miscellaneous\BrightPointAnalyzer.h">
<Filter>Source\Vision\Miscellaneous</Filter>
</ClInclude>
<ClInclude Include="Vision\Config\MatcherConfig.h">
<Filter>Source\Vision\Config</Filter>
</ClInclude>
@@ -791,6 +803,9 @@
<ClCompile Include="Config\Roguelike\RoguelikeCopilotConfig.cpp">
<Filter>Source\Resource\Roguelike</Filter>
</ClCompile>
<ClCompile Include="Config\Roguelike\RoguelikeMapConfig.cpp">
<Filter>Source\Resource\Roguelike</Filter>
</ClCompile>
<ClCompile Include="Config\Roguelike\RoguelikeRecruitConfig.cpp">
<Filter>Source\Resource\Roguelike</Filter>
</ClCompile>
@@ -968,6 +983,12 @@
<ClCompile Include="Task\Roguelike\RoguelikeBattleTaskPlugin.cpp">
<Filter>Source\Task\Roguelike</Filter>
</ClCompile>
<ClCompile Include="Task\Roguelike\Map\RoguelikeMap.cpp">
<Filter>Source\Task\Roguelike</Filter>
</ClCompile>
<ClCompile Include="Task\Roguelike\Map\RoguelikeRoutingTaskPlugin.cpp">
<Filter>Source\Task\Roguelike</Filter>
</ClCompile>
<ClCompile Include="Task\Roguelike\RoguelikeConfig.cpp">
<Filter>Source\Task\Roguelike</Filter>
</ClCompile>
@@ -1127,6 +1148,9 @@
<ClCompile Include="Vision\Miscellaneous\OperBoxImageAnalyzer.cpp">
<Filter>Source\Vision\Miscellaneous</Filter>
</ClCompile>
<ClCompile Include="Vision\Miscellaneous\BrightPointAnalyzer.cpp">
<Filter>Source\Vision\Miscellaneous</Filter>
</ClCompile>
<ClCompile Include="Vision\Config\MatcherConfig.cpp">
<Filter>Source\Vision\Config</Filter>
</ClCompile>

View File

@@ -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<RoguelikeFoldartalStartTaskPlugin>(m_config_ptr, m_control_ptr);
m_roguelike_task_ptr->register_plugin<RoguelikeCollapsalParadigmTaskPlugin>(m_config_ptr, m_control_ptr);
// ------------------ 萨卡兹主题专用插件 ------------------
m_roguelike_task_ptr->register_plugin<RoguelikeRoutingTaskPlugin>(m_config_ptr, m_control_ptr);
// 这个任务如果卡住会放弃当前的肉鸽并重新开始,所以多添加亿点。先这样凑合用
for (int i = 0; i != 999; ++i) {
m_subtasks.emplace_back(m_roguelike_task_ptr);

View File

@@ -0,0 +1,271 @@
#include "RoguelikeMap.h"
#include <algorithm>
#include <limits>
#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<size_t>
asst::RoguelikeMap::create_and_insert_node(const RoguelikeNodeType& type, const size_t& column, const int& y)
{
const RoguelikeNodePtr node = std::make_shared<RoguelikeNode>(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<size_t>, succs)
DEFINE_GET_NODE_FIELD(std::vector<size_t>, 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<size_t> 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<size_t> 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<size_t> 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<int>::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<int>::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<int>(index), node);
std::for_each(m_column_indices.begin() + static_cast<int>(column), m_column_indices.end(), [](size_t& x) {
x += 1;
});
return index;
}

View File

@@ -0,0 +1,117 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <vector>
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<size_t> succs; // successors 在 RoguelikeMap().m_nodes 中的 index
std::vector<size_t> 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<RoguelikeNode>;
using RoguelikeNodeCostFun = std::function<int(const RoguelikeNodePtr&)>;
// ================================================================================
// Q: RoguelikeMap 应该支持哪些功能?
// A: 原则上我希望 RoguelikeNode 和 RoguelikeNodePtr 仅在RoguelikeMap 内部调用
// 外部操作对节点的识别以 index 为准, 因此 public 成员函数往往应当接受 size_t 类型
// 的 node_index 参数
// ================================================================================
class RoguelikeMap
{
public:
RoguelikeMap();
// ———————— update map ————————————————————————————————————————————————————————————
// 创建并插入一个 node返回新 node 的 index
std::optional<size_t>
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<size_t> get_node_succs(const size_t& node_index) const;
std::vector<size_t> 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<size_t> insert_node(const RoguelikeNodePtr& node, const size_t& column);
// ———————— constants and variables ———————————————————————————————————————————————
std::vector<RoguelikeNodePtr> m_nodes;
std::vector<size_t> 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; };
};
}

View File

@@ -0,0 +1,333 @@
#include "RoguelikeRoutingTaskPlugin.h"
#include <limits>
#include <numeric>
#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<MatchTaskInfo> config = Task.get<MatchTaskInfo>(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<MatchTaskInfo>(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<Point> 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;
}
}

View File

@@ -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; // 节点间连线方向判定的阈值
};
}

View File

@@ -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);

View File

@@ -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<cv::Point> 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;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "Vision/VisionHelper.h"
namespace asst
{
class BrightPointAnalyzer : public VisionHelper
{
public:
using Result = Point;
using ResultsVec = std::vector<Result>;
using ResultsVecOpt = std::optional<ResultsVec>;
using VisionHelper::VisionHelper;
virtual ~BrightPointAnalyzer() override = default;
ResultsVecOpt analyze();
// FIXME: 老接口太难重构了,先弄个这玩意兼容下,后续慢慢全删掉
const auto& get_result() const noexcept { return m_result; }
private:
ResultsVec m_result;
};
}