From 74da94d4462527c3aac513c0638b4fc08611e5ec Mon Sep 17 00:00:00 2001 From: Weiyou Wang <44151844+Alan-Charred@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:54:19 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A0=E5=85=A5=E8=82=89=E9=B8=BD?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=9C=B0=E5=9B=BE=E8=AF=86=E5=88=AB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=B9=B6=E5=BA=94=E7=94=A8=E4=BA=8E=E8=90=A8?= =?UTF-8?q?=E5=8D=A1=E5=85=B9=E8=82=89=E9=B8=BD=E8=93=9D=E5=9B=BE=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E8=BF=87=E5=9B=BE=20(#9812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- resource/roguelike/Sarkaz/map.json | 21 ++ resource/tasks.json | 138 ++++++++ .../Sarkaz@Roguelike@MapNodeCombatOps.png | Bin 0 -> 11769 bytes .../Sarkaz@Roguelike@MapNodeCombatOpsGrey.png | Bin 0 -> 4766 bytes .../Sarkaz@Roguelike@MapNodeEmergencyOps.png | Bin 0 -> 12820 bytes ...rkaz@Roguelike@MapNodeEmergencyOpsGrey.png | Bin 0 -> 4976 bytes .../Sarkaz@Roguelike@MapNodeEncounter.png | Bin 0 -> 12845 bytes .../Sarkaz@Roguelike@MapNodeEncounterGrey.png | Bin 0 -> 5248 bytes ...arkaz@Roguelike@MapNodeRogueTraderGrey.png | Bin 0 -> 5260 bytes .../Sarkaz@Roguelike@RoutingRefreshNode.png | Bin 0 -> 2674 bytes src/MaaCore/Config/ResourceLoader.cpp | 6 + .../Config/Roguelike/RoguelikeMapConfig.cpp | 47 +++ .../Config/Roguelike/RoguelikeMapConfig.h | 28 ++ src/MaaCore/MaaCore.vcxproj | 8 + src/MaaCore/MaaCore.vcxproj.filters | 24 ++ src/MaaCore/Task/Interface/RoguelikeTask.cpp | 7 + .../Task/Roguelike/Map/RoguelikeMap.cpp | 271 ++++++++++++++ src/MaaCore/Task/Roguelike/Map/RoguelikeMap.h | 117 ++++++ .../Map/RoguelikeRoutingTaskPlugin.cpp | 333 ++++++++++++++++++ .../Map/RoguelikeRoutingTaskPlugin.h | 46 +++ src/MaaCore/Task/Roguelike/RoguelikeConfig.h | 8 +- .../Miscellaneous/BrightPointAnalyzer.cpp | 33 ++ .../Miscellaneous/BrightPointAnalyzer.h | 24 ++ 23 files changed, 1109 insertions(+), 2 deletions(-) create mode 100644 resource/roguelike/Sarkaz/map.json create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeCombatOps.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeCombatOpsGrey.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeEncounter.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeEncounterGrey.png create mode 100644 resource/template/Sarkaz@Roguelike@MapNodeRogueTraderGrey.png create mode 100644 resource/template/Sarkaz@Roguelike@RoutingRefreshNode.png create mode 100644 src/MaaCore/Config/Roguelike/RoguelikeMapConfig.cpp create mode 100644 src/MaaCore/Config/Roguelike/RoguelikeMapConfig.h create mode 100644 src/MaaCore/Task/Roguelike/Map/RoguelikeMap.cpp create mode 100644 src/MaaCore/Task/Roguelike/Map/RoguelikeMap.h create mode 100644 src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.cpp create mode 100644 src/MaaCore/Task/Roguelike/Map/RoguelikeRoutingTaskPlugin.h create mode 100644 src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.cpp create mode 100644 src/MaaCore/Vision/Miscellaneous/BrightPointAnalyzer.h 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 0000000000000000000000000000000000000000..1db0c6215a417fae167d780c180d8e98b96bc991 GIT binary patch literal 11769 zcmVr001ut0ssI2voytS000B&X+uL$X=7sm z0C?J!m0M^WRT#(rn>o7?66Zl`Q&UL?FKsAk*`%Tgwb8w0v+cTBHoMUVK{m5zccwd; z9cO0Kv=kqTAc$a8@JaCgR`5xP_x8mWQ5q;wfv$LFQq4fNnX^kEQvW!W-f`Fs`t94-DgZhnst z09-lX=I?CuRk$Ssp}UA16-!IuSv99hjt{Y- zG^g9Va#|@qO?(gWYQ1r+K)jRq8+{=+JP zQ^X_0CsaFA^!$Z4t)FT&N6=1{S~}L&p{*6p%)o?gmWCRG~tn2!~G7q^WId4j`z>J9&=-E;aYbt1u}p zBQf+LniN&=2St^DE&+uyI%-{fycJyYeHv_lMcE;mhgp^#VdHE95_^I@$tGEnq7&@! z_OtoU8r}6=+xB@LO>kE?N4g7?w^63+FQjO|x^*vB2AjiP{H*z$Dqnu}`o%M!@9JpR z+3K@b`Y)ad^h||?dY7)v+^+ZGU&B|!>vY`+Z*~;nAH$pU+3jSR3sURL5l2;OhiT_M~SzUk$se#plb@S`LYcFiH32?RBMVYjYXr6 zNioCFxRlZ5gCkN|(*}D7Q`|i7Mn>RCWW;B}7I%9d{ zMUoA&9ovbwB*rmWz{C(Jrzk@u6)Jy_f}#RNswfhuqDY`fK|%rv6#|Z95(nfZwq(hY zEy>zO%SbaLY1TJyfA226ozuJhvk(x}hU|*`K1O9MX1-h0_RdZLP@q#U4S6PSe@mR?cQ-;z8xv98Mkat}j zp(8Ioz^HGz_AP&Rl& z5nRu>@o?oY=qk~VI<^VhYjJ}gW$sH4N&q)J0w>Y5^sRK1)V$B#oxRI%UeB|l9aBqc zSw@wAO=isUvAN}AgCi%F`t!Y@O=605OK(+3qvc6eNqP`cAlq(T^pPtUSGB6KcH}^D zZ>*OALJJn?kAhIre)21KuELKuk3aj+V3dCVZpyqMN-ix*kbuK8G8V^6!eDqqDTT&# zLMuQ<-goMUl`_3}7PtwS20$h#8fdoU+yXjmh#a5jx4+)M)%?t@xZsU>l-kOpn`-NZ zd-xM&Ajy}ZKz9Oa)Sc7J0EZ98Jb)5@{vOXvNkdkTwhxA(c3AY(D83 z>{}@jCD@_JQ^uw6;9>3oX!+GCOvp`=g(Ibb+cwkoqsX^mx+)?!mMvINmoX*TM_Go$gAF$qRT zM^J$T&0XR4Y}VhcyPbi->q%o!D^3crdzI^@x{y>w?h z-WXvppM_K^GCDSBNnE8Gu_Un>fh6Ooi_P{r^PZzzT2!T#?>_wr@DnpwMWqXv+XFKe z$&LhOta2!W3RvRpG4)V^>Z-&*7`v)5q?1(R95G2*y0S)PWy`8WX;BYik-STmT37<5 zfH#DpF%?D?n4v~acVa1%xuuz2PgHrac^xvv%pIOvKCuw>O=pyh50L;-sjx92xHq3D zt+9S0XXKE$aJZMcpz<6dP^Iail?op0=!Nx%l5(I!ISw590@_a0I&g$iVo_o%JQKS! zqcgPdw0UvUj|(nxGGWEqw3<&$eerEOi7K#F9Np>e>GlN@`sX)a8p6?i=l6}VMm zc%>C^ZaRr8i;!3?&_XE-@VQBBW`I#%FA=uMLNg*{m|YC1)7gTAtg93W*&5iO?ct#| zgb`^2PUB*fW|MI-^sm<1snoB%it>=@nO;|RwTbGA=3+s1lcM_Dzx3&(H~aWQXGeP( zdQEmyK!0wz8+Ei7`om+k4hd{!w`!H!l*5oK?6^jn622(I3b?!3U}!te&UNm2$I7WQ zN3LGl`rp6xjLtyZY?A+0T>=6K6UY4Um z&&5gKNZH6FSC&9Oa07<$V!)P}iMm24tQKE^2%r!%+B}zv@Hxpwz@L2JZ9l4HM1oZk zPe^Q2w-k|@dNN|#_kB$$x}kvdB@R5(C9WLmA$Vm!=uCX;b#ON`_tWPzmdx zKPt4u5EExzFt&S>a<2?5c;9A>0P2ej;M;w~iovR2U$V|gf;TvaaMp>UNOrnWAD`~N z@AiNCpMK6$zqKwM7cV|_`T84M>(};nHpUy*hGqXIZj%*Bt;0tTh=}SzOF+-|=4fkU z2=R(f`_oU+R$sYGPEieIukFSeHE}$ zQPe*?(_QX+lXm^wT0Ds3eyn0m>Z-OksWB@&u^^G2SIwF|9J0w!2nC>kjg*c7W@Vy~ zF+;kUB@MVo0hE&T+EfpQH}QX4S97j(7-~sV7T_|Hp=FR4K_1+kh+%nHVjBFYB!r^g zOEcuDkwfMH_;6h8jf3H^*ojo0`v=G>~Ej>lxx97&G7~8+s2>6y5|^Qq__n zsYJuSkUM!<6(uTNX+z1j&f0>#wVM+9NnZ(McuCq4#aj^P#G`qH(}Q9SiSo%;zVeN4d^5N? z?e3(%_a7hqqu+mu7FC38Jvp}QTd?Q=6s(SvPNx1~90s=0I^TNe+;9HsSFgUgd6Sqe zt4IIjC8z-3#;KqJ9VWGl>Xrh48?3yuJhS}nWBp?bEkjMV)^{&o*tv2s9}m5AY8^QT zZ@~g@Wm&T?0n`ekrO_c#Xvp3L5$=8fhTcL$>15*+2!SqfNV-b~Z<(HrQx>{9w1LQL zwtNY_d6AZ7Q8k*pZgnNV%{8HaZEYuyA%eP=$awYP5cWfTcR>yjKesmCum6+(x!>bK zx_Wu@>@!!I(6}WwU?jR*tlK?Fc}ghrO(0} zN7GrnK;k|QDtL+p!BW3LkB9&u?>9F|RZ=;H(HKiY0_PaF5*53O4Oj`lfTykZIiWVs zGs1ksH*OlU5xnh+p-)42d!^>oVA`)6lX!JDWgRYAxH8)O>H+N0Xf!i3gIC}V^{4JR z^6Agq_r*VW%9fVg9Zot8^^EQuNQ4%iDKRiGgy)4L)OTln80H*`QlW{}Pk;Ww!^h@J zMus*LC$!?hJBGL-prf;g{@CjIOVpyZT*az0h*Dt_JZE(X&rNav3(_ zf?xeJ^OXblU=|M?QAttJv<-1geGyTM&;cK0Jy>Q7wtXGY<4}j5o=Gd|NiS@~dXY^5 zO?{e>2(5@`d@HdZjak6oyd_SR=L~!aIU{+Bky>k`fdg+#3}%*YJACHSrK?O3^`HLH z_rCS@SF)_U|G|?#_L=*lNQZ2RQ<;dl+h=Lhg|j)b4LE^;irmgDC1F>B!odBB(tqt| z-*fxvl`Jj(@V6gB#>m7^uWw-+n6|_Sfpwv)q$8>S|1Fna-~7VwKK|;9 z*DhY!J$?G{@e_;vUUK=B?csXfOX8y^7eg(uuq?IXanYNJtwX9%gA(H%7|e|J@_07- z>@Pfc*O?>RYvcd?Zy$d7`D@oN@7{IKN~dT3+iyQQuJWK8o;!bI41+IAM?ogE7E;@q z$EoWq&oW%AL?|%P@zxuiR4jmh;+&aewg$P-S%+4hx2kluszvc1V zdGDd0{`tRr;?Ct)U%37s{?%7sdui3z|IvT_?$`e0`O$9fUl$MdY+CHCZtq;#aMt?u zjP|xJoj?1+cfR(@Q;)$KstEIxrWTzNLY9Sfr3-~3?@|elL|17(s%f{5F;}&El%;o* ztgwYDEaX*GBiEv-#{gdP3+F7hVQq%(O(w%ccRKOFyN8$)qhT_jBkvN?+n@-lsn$a+ z)SId-s#r>}<_KC-xV7APK3d^TeBHY8^Z+_(RN&H~Te`hLyMf>Qm50tgacT3?CI)v! zwf4s5um0mdJ^$j3Q}?cX_UHdnZ#D)%05PRx+)*t)fH0+rjsc5aKmPbno&j#2dhFtF z{mX~1y}8pKUR-%&^Xq?lc06j()1ObU+Gr+OJb5_ocgwVR{oJ$9J@(a0ubj0-siKIX zLBiK7>~c}WX;=HoNM4qxI`ACG4LySS&e*Z&A8wZ;pMiV0EbI|leXYqU_j9~Os z*30*br4Qks?JOpe90TJ^9y3WQ2rL`ufLWETCqPEHKv4zCka|iEV>HsNwUL;rGzx4) zhGbnvx?@zVRKyV`Bc$SHYH{&M@O^3f%HD8e4CBh?Og&+B;dsl85 z02x@hx0>)U-44xuRxm>uG$75{0~faA2{0h-bSCCk`fq9^zw{ zpFFpjaZ$R-^!9du&F4lg%ktg@ne; zgicoNV)8+tJF1yNhJ?o4!eW$>{d zKCQdvwHL4b`IlatJCLkj-)m)Xo6x>}n7ieGP-5-;mGO-ov>7ZOD)X#zM7R#(7ZYQ* zb?w=Kf?A9uWu#{cet|+N`H&?vN`rxVNgvqre&|%jgw7IfTu4}#Okewh>AZL38V3p= zmr~lQz=*q(^=>lY*j&J;epd16gb+F)C0SrWWYan73%<*AS+n3mX?YLy%BgX91p0ym zOfOy}*Qx?ybSM;RJ%p&Ns7b6lwuYXyrw+cG<3$xpW`tn#!kSL>%<@cU(5qPO=_Hsv zVBWZJW3bR$Se!Y2a&fdh8X^E{trj)LixYk9*2NzJZW4X|+|@_E@`AHP+!I^tTa{2` z^hz9z_4V~@mnsQmQb3j~kZu?wcay%-QBkI>NBCTe@dShrDGQdTLVB+c zvaS}*Q45g7-Z--Z+e#d<Z5N+S>NJ z^P(htdXt0QUGMUuV8#r|?QFarAR8--i2j+H{DLY*qGTjlkW8Xyr8FLMZBU0#f2qJI zXxI913*94CTvr+rgi6owvn#WfNfje-j9Vn#2FlLX-~Q&S*(9A`?7}Nry}X0U4}JQ? zU;f}7N{JVryZXr2&IN^KV9`Y6t`>D`9hKu;M%wfYw&VlJqpj^?k_Pl@+0RE8{gyr# zfss)V56W@jTrLC6L{>E?TSB&q79}6iNDnGjKOBnYactQ*8xT^g=%pj}X&m*|<0@St zys2BbR8cb>l@YiMxZ&Q)Q^8yIRTW?)RbjrsDoI8Gn8}EiBKJS|z=2jBekLrmel#Nz zbyO=*JDsGb1|$=5Y=t6SRMsuAlTh{G*r(OQo$cK({>~G>{CEHRmw)f+H(%Yrupjx+ zd!z(zt^haQ$j;L;OY<@>J>YC+^I?X&0K=;g=R>b7Jpy*u$fHoQaVL?d*9;mmBB1Cv zGF^6ZoU=$J@PoISs!(UWSyKMz z3QNR}DO5(520fFJXbCNhS&1Le<`~+Fq8)rF545-UH)!?VIC1zf>yZef)N&n*Y!S2p zCTcIKD{^P1UxjGO8_9xVI}doLymSI$8)qa;lN_&hHYS(PZ`!g(o?q^L=I?(14K7_= zfAnkTFgI1*e=itY0o>t|QTYa=>gW5i=l@J&7*3&r6KHU!yOYryJv z=lLj!XR+ZT%gdqwPc()y^GMAS5+&iPrk|~B;c|jKTYD%$me_7X#EjK2;2-16scM|< zURi%-I9?Y#a)?q1?MOIU!bBL=R2nV7zm{wgSD7GcWU&lNg$|DVQyEr*QIFjCGmHRg zCm1j2jR~+UR)!L7J!5m0gbMB{P0}$-F_2lRs)&M-O|PF{O(z+PSIPu9Bo5eUK*|!) zDOF1?vb5;T=#Ty6JKpxrTL=NA8_Y(bbI5t&U`ge~5^nS^CWmVQ*?hlq1YrqODs;OnVIWBhdoj11N9(sM&g21Bv z&FR?f3!nO1_uv2iJI+4+#@_Da^u0&#d*5w?g)YR}WOtH}^I}q6;K!4c#nnic4c3FUJEk`3 z?GGIL>z{w{LqB@vnI|qj^o3`he(b{9>h9fVj^1+H5&~=aD9?r|j5lCt+KKe>!pWuH z(o8Eb)8PCZVG z4O(-shYcj^&e-zWtJ$jU#C?G(zT2fkl7N~nBQl6FidR-1|DKIhrF0!jC&Lo*s)=-Y znc42Vb`(SMM@$v1qij#avCtM_t?l6bk7O{Tdp%+uNnOEDrekJr84QljnO;3^!V4$J^S6)FTQdEcN1)0f^YIP7m{2s zPjfV_l<^>Ghnno{`UmzpDUmhHI^PS5HjwBai0z9U`0*E*y*SXu_&o{=h-wVtHx49u zXpm52^wfSpmQ?-#ObE0>LGuP2x#UkOZ%9vT`vb@06(xa0V32R7FBo_hR^S6^H$ zIbt6rkuBUCFI*mO46#~dnGl1%U%w6H_K=xq%f zTrS+FET`sMp5?pv%h2%0|4cnzj!jcLj+ZRcD2!}Ai00xS5&;YK+}&Fxk_3cm9L-26 z*iIs^<3fpC1O+J-aA8fJgyTbl5Wh&W*d3Ai`r=%A#5EoM_Xo9-L8LXF|}ta-a!RcT9mUU5Dda5LL()= zaFsIPt0JV}zoSW{6}6#hye}wfp`0wG!yBWA;O>=xvv}rh@H7xhc=zPS_ZlnDIxP>S z`3^~3_Cf_wR5Z7Eusb{3pPzTm?!LLzU7AT2x^;V7lv95;IddOQfJ~AYsU=?TI=x~d zdGXb?Cmz1Au{QF@d>%JE5lM!_w;#f@(h6%crg-h8tF!aHGH(m9No2AAB7n2D2b$r`GZq6!M~@mv%r6-q24G<8m49vxT$rL4eUWhjUtwTwt& zs7~o}R;mOM9-0ZxO#{&kwf$^PR~zt|5r$q%gTwQO4|e-~m%FXYH-?wTD$?Eg#5Z<= z8LJz8=6YU=geDm#Zp5NJ;2pJ+g>JP1z6a?Qr^3UOHO5VRRAP5b8^TfB# zkH+?0@3|E@K<4Bx?wH6u{q2Xm^e)QO*12H2r?RLD*^_fZDMwiH@1Q%^~F|>CP{$9KH3_;gctl*c;D@vu6MR-WIx|m|Kx>j#!ZujsJIJ0Z*8S{ zVF3WZ4o!=~mbFIS8`sZ2dv*2tZg6v2S(*RHC(nHHXCCbKVj5clTpo0^5r6FN^@P9v zhb**V&0p=mT0~7qt2dR*4c^ofp0Kp=md^C}6mwNT0+s&eGfNET8&g9}1hl(=CRRa& zECMg~lypEq!U~F9RuteQ0VMj7Il3B45;sg}^z%Z-yKzKOe}VGotsK4M=q-00S~-CS z(~Z26)E(VFHrH9|go0dA-}v@BmVV^ZXC8R)cBNEsb1HKC`m1aH3=Pn0FEx5y-RbE# z)=G(Kh%szKsFP87{*@d4d1rgiP3xDImOlHrAOGAh{?zi}fmD2v%JDYJiX+nmc2~Ma z(+f(NiRw!mdd~#l^)>{=~&m``b?dH{L{} zoZH$`WY`=0n9qS9i;__5mn1k>L3<0wYn^Gp#yMM})JnHE$vCqdqjO0qDj_V}7i5ha zU zeQ43+zbe&6%p6w+qhaAJ1CC+j?NsiSqTJdXqtseE>o(T5Cgaq19}b5Ip#8u{9{jm~ z^mmS&JY1E|bAX5x%Uh1?I{a~t?b6n^^rFmJ+s3u=$sa9XG3^yhts@Qsl^m@p**#)k zM*~J2Qe*M0RoogB4$Uj8cIxf49?i)Ej>6Ce8)Dayozt=kg^ynH3I)&ZNJG&Ob!fp! zE;lNItsJYkMl)4|+$MSL@_gpNiT0^$T6wPyv!WD^)Hhq- zmf*rH#10Eb@7V-4A|eiG&Wf8;eD73MqKRwo4mUe-&*%iW=ZHh?Zw)B4o)L2@Fg9>; z*aKXnFoyW*gdI-`7=t7sbzltbgH&NtC1n*jB_u};XjM_vix57U>30SPP0tTPvSoN> z+oolZ|xt;N)XRkc@*c%mN#afmU znnN0_biuTpt_J#nKdA!dn`XV;nHWlI`^MfwU-;gqKKq_7FU+Su{M|1-`N%Wrh&U;; zZgXu%@NHYE1>IiL7f&y1jJHTUG*WA8W9ZR?jeYyiUw!^trXxixi^FN-=v+}2Y_I+_;8g_j7_2(XYsr77YZ~sVHd*?BCIx$2C0Dy-n zH)H7I&2s}5fF}Yvp=+7|oo>=)$PnlGP%5)Ki8Gd!rT3H~(-rJ&A+gvm1R7L@7*{)m z%XZwh3+!ICDd~V13~Wf|1Qnzfq*bTxb?UBxq%3P;1L=^_TSy9F`UnJ>=Do!Oy`|;O z+??qoSb{4pXXkR84JU*fPSOZv_}Yqh>rkWg)Ike<+3s+Deg>tr_R+7N``VXZx^ig; zuRd7mEl*;asSobh63%mr5*cXACMAT#crR6va?U|dY9k-{vzNd0``_7IA7*^SaOXX@ z1^W`P;_Wk(ShFokZ(R60NsKv)q4@{OUddIq{a+04rNkxyRVb;w%`iP)P(qL)X^U>cA_kxq)qINPpfk1Wbab{6}HI;UIX$;Ozp%rs#fZzK5))ca1X zTLBs;dV6d1jX!(&`KK?#4{iH;(;e8cj`R?Aj=o^0vZV3Tw2&&^+ey<=v3z78Y`D8Q zgVV4x*PS!xjoL#y)aq7~Rk zNRn?J;_M6&zI=CobH|uyrq>ssa*#T=!OZyU`}!bh&89Q{`UvXI-qnTK!+9|QZluu2 zjTpS`(7oe&nAKU$_wDm49pXYnwK}OL2gIRWxib(8A~Y_vKGk&3Pjdk-^_OMe6J8LoH;a0=AxE5HU@#2xkT^+&5ZHJw{td~Y-t@!7887g zyg#@Y;M(-V554@-^Vi1146i=d(n+i}YR(*+dwZVq2;a^qQsNNI z<&Y7lnzVLpB9kPMuMyMut_4c+i7jlBBsZ>K!L&&dgBx~tw)%rXZC#d*cUN~O8zX{~ zl2>25wEE`in=h~J+!&)VCNV;5$vM<9$W3)s7!%oqq}W;tkZO7vmv_KQ&2-~#FBv2{ z@$xM|lU8h@P=eRk#6G;@2?c(N?f+66qFpIfh5E1w&=Hna%yAVcSG&B~5#6XX+_;G+gU19Ecw!6~L>szSN)>5V6nUO! zc)Rf0D~Avr8Z2PvWu1eiMpt(LjBqlP70_I*zPWMZ(iZ0DgJmm07n~HkGm8` zvy%{opM2-%XT=;irW^LQ?7Hw!;Sy^-&^c5Dc}UM1uXSW4TQ-3p?`ddrC!JL`GO>vg z9M_k&h?4}Io{oVtrK6U>1<0S797Q`t-Od&A*RssOT$w)ZJj^h?J@(&=oh-;mTG)Ir_tlWKvK+gMv2t`0evP4Yc2$FPpQ2Ua&E5C(1H z3M%Z^~ZIdKsr001ut0{{R33y7i%000B&X+uL$X=7sm z0C?J!m0M^WRT#(rn>o7?66Zl`Q&UL?FKsAk*`%Tgwb8w0v+cTBHoMUVK{m5zccwd; z9cO0Kv=kqTAc$a8@JaCgR`5xP_x8mWQ5q;wfv$LFQq4fNnX^kEQvW!W-f`Fs`t94-DgZhnst z09-lX=I?CuRk$Ssp}UA16-!IuSv99hjt{Y- zG^g9Va#|@qO?(gWYQ1r+K)jRq8+{=+JP zQ^X_0CsaFA^!$Z4t)FT&N6=1{S~}L&p{*6p%)o?gmWCRG~tn2!~G7q^WId4j`z>J9&=-E;aYbt1u}p zBQf+LniN&=2St^DE&+uyI%-{fycJyYeHv_lMcE;mhgp^#VdHE95_^I@$tGEnq7&@! z_OtoU8r}6=+xB@LO>kE?N4g7?w^63+FQjO|x^*vB2AjiP{H*z$Dqnu}`o%M!@9JpR z+3K@b`Y)ad^h||?dY7)v+^+ZGU&B|!>vY`+Z*~;nAH$pU+3jSR3sURL5l2;OhiT_M~SzUk$se#plb@S`LYcFiH32?RBMVYjYXr6 zNioCFxRlZ5gCkN|(CZ-rlqB&q@<*yqobmt zqM@OoprD|ho}QhZot&JUo12@OnwprHn3tEAmX?;3l$4W`lai8>k&%&*kdTj$kB*Lx zjg5_rjEswmi;9YhiHV7bh=_-WhlYlRg@uKLgoJ~GgMxyBfq{X5fPjC0e|~;`eSLj= ze0+O*dwP0$d3kwwczAbrcXoDmb#--gbaZobb8>QWadB~QaBy#LZ*FdGZEbCAY;0?5 zYieq0X=!O_XlQ3=XJ%$*Wo2b#WMpGwV`5@rVPRolU|?TgUtV5bU0q#VTwGgQTUuIL zSXfwAR#sJ2Ra8_|Q&Uq?Qc_V-QBY7&Pft%yPEJitO-xKoOG`^iN=ivdNk>OVMn*@|DJdu@C?+N*B_$;!BqSpvBOxInARr(gA0HhZ9UL4S z8X6iI85u4!G^+pr44+9vK~#9!1;JUb?e}>Q;OBjR|F!l$d+&3Or_3gC(o9aA5(`Q| zLJVI3iObU1Ik96gUvLFdKLeKtij?C_;+jDMfj|g}i_%o2N~*LO?bJ@{c=p-jTI>IN z-#$Fcf&xfnQ?t-IJG^U}9m^Qw7Zv{DJfsiH( zv01(7t0OR%Ipf7;t^NJypb1b(AT*j0N+UG5j)-O1{)?OaO6B=MOsAxh2!--&H7L;` z6~eN)p5$EeIU}0w)$#c^?me^2!;1g_0A?~)LTDfh*FmUS4;|%VWzLIJAY_FSXh1VK zxdKjaxn?pPmYLJ~;@4gjxpV^~i!4ATY=T(C=0F$|8au2PH>D!Wlr<Vtq!a38>fgNzVR=|@#fCe#d-CY=QtUNe_CC8 z+qpq5dxBU18i{7R2><|q7&d?XW!&H{7!Hed*#OHq=*`nXM9f4iSG!(0I^B$zQy$WK zBr{ee;XccK&wrg3l}KU9?vz3cKxiqHTb_LCkKyd&pQ0s)9-9K{^P4x`gioI$?ms)e zVK}^9HJqF*Lgz$ClvtVWxqq*|mA+DV@kKq8h}dob007XPdGuT0k40Z1@#pL={|(dC{*b|5n#%>_Usz?ci~Ke@uMzyH^O zbJlb?A4l;0H{c_;8u!1fpYcvT%gG0}>G&J~vJkijX^}-hbO2BZfJQ(O-u>m>E5EUO z^=F=jw@P*}&mHl-UmQN`Pi<`+eD$Cx%cG~`7MoGSnft;36!uiAbsAXSXY5d;7N zfkqGquhiq`o^t1hKihtW^P6SaT(wQPy5+ks9ewT-Uh%}-Kh}qu6CQovQ7SQGt7abW z4$+p`m6IWsvUuKNl;wo$U?34>011IKhpb$GdWety_>(93q#ypMKjWq&Zpe%}yz<&h zk9_fT>r?MN)^&4sa`1phKd@P%gffZMa zn_vI>h zxxRf~@B8q{h5}R%g494p$b0u*Jghy%^Br+|ldC0X&avb&mOu<6g>kB`9?Ill-Z}AV({`Kwel#``%BBDA{WEa(=XXT;NShZ2o8Y%m_7A&)Zbz3p2 z8GslA&;XaaXtK{sAc9w4d-={MPd@9l{l-6>^J1{u7q`&JY7xXRQORsT)|j!jIby_a z8T(Nsh1pTe5l~4eu}m6R$9Z?yJa}VG!L;|vul5gr;%FXy`ZFIrWqGeEnK(wXG;$gu zMlnT^%Gg^}HJV_lo};&i5n5$vz#P^E+iRfV0@llkN$Xc$-hJ?sTReVyL(c~rl?6wD zp3Jr_st6%KwOYHwn2b4l*&Ia77*UcI-5FYTnMUUE4v-!+lt5v=^78KSCywP|JUnpz z|2|fyr-%J2PR=e>$Zjf(=&56^>}pi&wrZ^-vu0Up$zBvqpa5DL$!59C0^UMs06%(p z_xQ(;s5Wr<`#*c|g-6GE(3z)~T4*xILQVt?jRAU6%j~wyQ3+%0HDj|b03l$U&Xogi zthcUZ0heC?>4V)9AHH*K^jzLtxjxyycE0;x&o)lRINCRsVPmKg6+!_B)+imaTF!-B zRumA`>oNdZHRgjqf9vXQ<}d&7%>v79=$i4)9&^dehj~ck?g#(=$C*nkTVv_Y9Ak7$L`$*@ zki%FMawZ!Rk&=x%Oh97)=4`A0wB0X+P% zD=L$do|Q|_N7kd4=IPyOjP>&8zj++dh#u7;DiJkG)Gi{^oI;fln_iAL=Vr-u0MQ|r z9tUx8+q)00Cfn0IzweofZsQ4Ycp1D;w3vgNSazD_?Bwoy4=!eJpLjTWMI+1_6QWHM zm8C>1Q#^EW=n$Lbu(s=?LmZaYWFV?0>YdArb)?6ZaoL{jgioS&e~u?Vsi7-oio%6E zfBS#j;O{;?IH{yyCMZOA&BF|p){q@H5sO-5DQ6e4nsV7Cpv2_ra-Gw1a%-C|uf9Av z;1+C&&wuK8Wwah;d(f7euK(lT`=2*F_Si(!y3L8kB=;%EeYH}$98GgKqO+Fe_;7SH z4y(xS)fmwFaI;_MlxB}PeEqje=%%q=^Orwy6hYOdrLlJ0a(a2v_fDVw$y!OHgEer0uX;+?+wcmMbi&tF3Ux*q!C zqg&mfM`LTdJ2`s0&RSo8>ciiA>87rZHi+4vi8j&uYqPtcbcaXYUR102S2b?t#!Znm4DjWysf53>M>$Qj0*`;8R8d%14T zaMM^!D3RBhGUEr)SUJd1s#8`A}Xs!2%c= zQXC3v&8C!9CHlnnE(I) literal 0 HcmV?d00001 diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOps.png new file mode 100644 index 0000000000000000000000000000000000000000..56588cdc58092395669576248d634cd1358fd218 GIT binary patch literal 12820 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 literal 0 HcmV?d00001 diff --git a/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png b/resource/template/Sarkaz@Roguelike@MapNodeEmergencyOpsGrey.png new file mode 100644 index 0000000000000000000000000000000000000000..8ecef13a9dfbf308222254bdbc9c08611bdd1719 GIT binary patch literal 4976 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%0APW#MlKboNw05} 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 + +#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; +}; +}