From c5a04c55dc40d8ff306970fde51b9d2c9fef99d9 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sat, 8 Jan 2022 17:00:12 +0800 Subject: [PATCH 01/18] Add: Submarine move in map (#864) --- assets/cn/handler/SUBMARINE_MOVE_CANCEL.png | Bin 0 -> 9945 bytes assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png | Bin 0 -> 10280 bytes assets/cn/handler/SUBMARINE_MOVE_ENTER.png | Bin 0 -> 10802 bytes assets/en/handler/SUBMARINE_MOVE_ENTER.png | Bin 0 -> 10802 bytes assets/jp/handler/SUBMARINE_MOVE_ENTER.png | Bin 0 -> 10802 bytes assets/tw/handler/SUBMARINE_MOVE_ENTER.png | Bin 0 -> 10802 bytes module/handler/assets.py | 3 + module/handler/strategy.py | 62 ++++++++++++-- module/map/fleet.py | 82 +++++++++++++++++++ module/map_detection/grid_predictor.py | 4 + 10 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 assets/cn/handler/SUBMARINE_MOVE_CANCEL.png create mode 100644 assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png create mode 100644 assets/cn/handler/SUBMARINE_MOVE_ENTER.png create mode 100644 assets/en/handler/SUBMARINE_MOVE_ENTER.png create mode 100644 assets/jp/handler/SUBMARINE_MOVE_ENTER.png create mode 100644 assets/tw/handler/SUBMARINE_MOVE_ENTER.png diff --git a/assets/cn/handler/SUBMARINE_MOVE_CANCEL.png b/assets/cn/handler/SUBMARINE_MOVE_CANCEL.png new file mode 100644 index 0000000000000000000000000000000000000000..96ca0c53797b1065156b6de31a2e26813cded73a GIT binary patch literal 9945 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 literal 0 HcmV?d00001 diff --git a/assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png b/assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0a5e24b45be6f27bded43020ba1e6b5c743b31 GIT binary patch literal 10280 zcmeHt_g52J*mXp`ie5mJqJW@5rAUz`9aK7(A|(`Q0s^8y=nx>p3S5vTO(YO5Ez+eU z5Ku9K#3TYz0}0YX3lIW?5E9;azwb|Ye|gusGi%nFHM7>N=j?r+=j?sX{C(HTgy)pV zDF6V#V`ge-3jiGF+;aUm$;D}INthquG~B_a&S3z+>2trYBY?N?^8mmpOMe4{yLTUg z!@*$>!NHfz3=A#>hk||m1AG90=*dF6D5osCkRFNd!{=~ke6t}h;3)toS#z@gjC0AE z*8tBKXU^V2zf3yiARx#OHwArpHgd|eFln|}y4~+~a?-sNkgLYJ#1ygx8nZ#6Fvpl( zWH|W%2f_0_;XivVbFt(RK-2c6j@GzDy0z(-Z>T3+f~GUaE}dD#eD*tz1^_mXLZait z-|HV7sRskN0Kp>)SI+kRI(*)7_nIT%QYK&qc$R$nNQNz-U%|d#0MLIP@ZPTJ!6`sC z01yC)y8{GN@c`Dg&Ge1~#w)X?gaP9vVwEQWsYd|$(k559Zgm3ggPz*Taee#@D6Y8$ zR5{MwJD&sia!L6LQ~X=Kc?J4!)xvLw6jC5)9OGdQc-;3_AWOm+IZ{Vve5 z5Awk_)9~`QoY}L>FlDMlE{i)WiU&}WVExgm^+XFM;A8yy5v~+or=du|t$3RJE0NQg zfHQuo$`CpLkk!LTzjgfx0Gn4f2LN;}op*hgEo|}iBmiJo{8aY+?Gt}~5~%se3;%Sn z>!UE2=O2c*{=D*0=ggfy=AVe2`saM^?F;rFwZ@ONygw>Z%>VY+ndVPQ8a(8gv#w89 zgN6UNf4=DR`K|=+W4HTHp7Xq{mBjCrHV(v{6wXgO2J|#lef^~D(sGi34G`TQTE#h9^LjNV@P``D=05V7Mxu+mqaq z$Acox;<`^-OOS(qEk%R_pvh_8PX?8EeP2y@y1q&@y8pM6$hAHp?*%6{hjcaXi%#m1 zf}$UbJU32W;YxXOZtbM|i!+}Co@Hd1I9WIuyt}$4*?hL+q$Bt97fPR{fLZUXo3F;4 zWqgVD(rU%c76=(&%H&H&e!TVf^QS*lTn_Kxo_;QF zl=mZV=<(8tW%Ba#?WjL_5_q2XnR-cIczhxD0`h#@1;u=eisQq1`Th1c#*{_#{oe4s z0fU@mcV*)QGY8J*XyrKM#OIK!>_K55{SipjY5QsrU$wgfw89wFba$?j`=?O#NaaOP zXXVX0oNbhC`dz$@M`^bQ>)F0HPh-MMVuNP7;3w>4gp*r0e4RtuWD);z} zrI$-1W6dJ+I8!uIG1kJ)Ags!_#y8Wqa4`gXg!gO1%%XNUnQM;Z&3^pWw_~t*IQ;wf zez~cd@tSXIX__PO$3>pq6J3OZ)R*N(aI|^K&6K{}$=o>1@m$MXdd(XLJqL)xj>A}u z*{JKP+-l&cW58We$AUi#v$!V%H*gn!@w?dBi=*1%+Hs-b5#J*Ng1c7BeyR3WeyQwTcJveV`zNfBrY2nRR|e*6 zUSyuIT_7?U8H9+zTcZL-lRLt zhJ2qZL>3@F(=y14?vd`!?o5=pbrbwr(ddo9e!>@*85Ab=zLuV@&pvMXsNPpSE<_9> zvAd!hNYd?=j3159k3Vrx#MIu$u+sN>enGZ`s8+}CANh1-;Rx!f$kV2ykB>?m%Rg3r zLhX3!@yG=zqg)DnlTATYM1c8kT1-D3dqPRS1GL6G~0!Tgwy3lYv!GqeyB8RdgZGR z+Jr}~p^<(yj{#Uf6s++kpteUz~|rE z=Z1UNElibs5MEryVTz)3oYkB&;xt~qIh)t0zwrFhox6(Xtrw6%mKV=$y(oBTZPZ~g6g*Keef3a%y|IXc$7Ed z;sBEs&he-Tu6b9t_ujaDan{kwVhHb_UV2`SKs!?3Nx7HB0{<&GY8&e2#!#J>NtJ}IKu1${i zOh?7ic%NSs)Fy6ZuXVLtk9K^FGJ@^|4Xr<$P8abK?dVyIpKdpavPKuB$Ck&Q*frW% zeH0E3n5ysTY2*&Hy_qU*sujFY@B}D6LN5aPT#uVHT5Y)!kHPN=9Ytv%w zS37q0B6lgmQK!}|rrcuEqw5d4Cs`{e_W4#|&FluBkAX9L_dnjp_d4@!@Ny>>v9k|{ z6Mqevd^Z`9G?q+FFO-lFy)npQDmiZUn>0o^GG$RGy zQwOP}g>F<=)@$`i_DOW%AiC6wgIXK`y3xMTLFz+Eng`+kvd7Ii=0st{r0e~mZ0RYA!r(FgBB5nWxEB65atv3LGF!;3x+86-1 z2r@IgZ67_kHjPgab8MB`RLS5`zbpzYK6c^mvr`%W*g5^Udhf>7&Ckbme<-7(ck58w zj~+%>GaO@<%VVXm1CP!d?A$R}ekxjiJJt5m?UVLrxD}EBf?tl4y@Z^r5U-= z;v8%p9vD72+p;(Mfjq@gCI21&A@Cmp{~_=n0{3w*RKi+667BgTSWDNqrE|8 zF_QAUVPlBS?Hw!-_|`|E|i379-sG8)@u@Z`l|Qd6a<$n=%f;bwX>3DL&+ zc0tb%6R-I;PPozfsR~y+Y~uE9Sl@7KSuWHOl5i}oq9_-XnGCd49|~xajxWqjOo}1_rfMC2jn39%F2%$NG+|4mZ4Kl02LtX^C?2*t z#UCa(b@n`r3bIHkaYqhNL`R5Gmay$K(E&(5%VwR~-ENzc70yVa<|pN*&wf)DHZtdD zS~KIaHv;n1>jjOHtzqhgRzoFNsSm2PLs@yOMu|G>>bYk9zd0`~OUDZkn2&w_2hxhU zZnzM#Lru?9u|ctLj2@{d$H;tjA2Q5~ZB)ci<s3)qi;r;draCuZ^k6f_BkMZ3ULnIwgJFlaUT@xk>^ z?+?2vozk|Mb@oyXYl{w(br|ZZD;$_?wvrxdSfxHhmKZZihSQXhN$-#{#+kF!2h z8uRc(6-Q^W?Ucx9nid%}F9wMS%87PIzt^H`CV<2p`1Rr9xv)3(CsJjP%8Q_L`S93(miL^n#{as$CZx!UT< z?BAmwjq&Wyl&qSKCXSfxD@ZM08B$MVu)8{SPOZ)phYn%sF94k7^e%h|QuZtI$liN= z_1svB*(AHzTqZQFNAIDKB5hy3iLWYqh^U`Sm$-?gPm>P=9m=Sv6V^hCOpN2ZYA**z ztTHKfuMCoJZN)Z5*W2xni1IF_Pxi}{3;47sykC!ZH4=zTC!lY;TgQvvEVZbVEg`-M z^1#r@EaIF3IRdU{**NUgRmV*RtznrFj+?7Tap;)|Un*n6t|s)A*Mx~-nE(Q2zW}n3 z@`p6*1+oK>I8AGH+e~7?zc~ucHny=C(xT6cQIrt@4<N)J~LYsy|M*QtI~H! z^Z}#Mzq8R)T^~+~cPclls#qqfr?8BKOW(N3W16jAE#qoqT7Ofh78lX%6c3}W_~oGY zT#xV7fN&-smYG}Tf|&xaY!C+zTS41U@$`g3XJ>F()f;2OuC>z(ZNJ z6C(jyg+kPCk%MBqSWmsbsjIqA^AgE1Q0t$Z3M-_zMJ&OX2;Y2W9;m*iPhr9fHWGdF zJ4?m*Bg6;3HNPEGETHMQEnmRV53QCfVpf@`e90c+Z&u<0FT{ z=fW#~avEgOgnZFtB=2qB6rbOjlrbPPAcwvlHJW77*c#v5ssMR1$!2nVG!~?D%|mKl#RSK<ZTQInuPtp*X1H$I@eRS z=OM5SPZfe3j#qu1cBXpnSOM*Z7<$6zYhWHNLvr?kXfd4jjgTmY>Bf*%M)%@K0U}13 zz*J)9lXOysW60E0XL2jpJ11E{vyYXCQBBr|+RNucz(n`xum~TxV+f3>nA=y_E9^~@ zU7yl&--8b6xC(>}kz53qhYKIUhxS#7Q?hm73a{5gAF%oMPdIy$@$oBFu8Lt+rF#c_ zO~CRPNwjn_A9BvDIkDIOiipvd4iowgOc7b`fr_m!O&OW?Qr2^(O-UHJFNC_?U^krn zaM&!LqS|>+FJ@H1J=|XvR~@$@0gV+v5M@{Vt8Ut=nWf~hCZ2_5+7if%!RH;odXzH_ z^mbjB?6jnUo%RDtILgu#h_j?=6Zk;Gyku}_;A4k-W8J%VqR}E|nAK`4{ z&iIwr4Qxfc5Ditu)JfaAFGP$5(HgD&xYl=1ENl<#&F{rDwKv&~i5h7&#cR}iX%x6A zUim(9WB#<@jEc({iA5)#osm%Z+h0ueV>49gZg#paoG3o+u!@cCQ%Ww8_R3yKrTS_2 zp$SP#XewP z(KM(SZE)pxoYDH&npfJCBefhHSMBHJ**4mkdwNOI=pB0aVXgk&}&T8Z$`oNi>HXWV4RWndM z`(1YFr}!fJHj#A$+Ny%B{xz851ZIYZRgdRi+#rTiS55nLhxly-ms9lZjkH*kp=nO& z-4kdIslZUfd#q;?-2X31w|Y#|^%vf%-RBc<_{bHehRV6e51F)Biu?Mh?(<084j|pc z#u}4#WkPH*LVuH^C1#6L)@xfP@a)H&a)wZ)^dh0dky35VvDN$Ar4t%xVplt}r-03= z$*h-fxFp^qlA|exfqE!VOQL0#BuA&Nr{Q?8_mKNpd`?zF>#3p4N&jC_AsKY>Hj1Y> z(%#~0p#D4@w*3DmK178mnsLEVxEv-gUY?{XzR*^5U%;=8QCr;@jVkVNUi3jodO4vf zBn$c?5D*cv)%T{h47hsIG#RDF45*+w;VPC$o zsG`8d2hn+%9F)_^o&v1^p{;dN>Tc!@v87eWzSFG*J{-}j$lKA1pUY-9ySbArn0W6E z!jw3xQRV%=6Eh+dhtb1f|L%Y%8Y8JnY_QzA{D469lE=b?Z%E^OG`owEmY`#Z2-CIf zB!FVaTBfK=Y|_y{SJQ6T-H1eI*?h_GSg%nK zM=d2BEdFJoKD65s&b-H8Dm_GXU9X{*{*%`RI>{!=N!AHM51)r?ySlE!;*)Q3XPeDwFCJ)j3&L9m~g|jL0sw z$eP=oWEqk8G#v&NXaOpb`f!D}L15zINVpq?(BmV~~I2usuGc^50l z0FCh>+gt@#ug%9X?6R{|#miNb?F|j(av>oZVYpsCa!2X#Y7c*z0KrA3exP1_{KUVh zyf{b;@^f%Att7K`&86zMO$?$czNys%>lG|ZVwid#7p|}v=}n=pyJLPdPv7XgH#$7W zJrsNB+M3u`+hs{ua23efX7oi6$dF-ly3fqPXzFu(`D8W+D6fvpI_s1wjd*HVUF?#+A2E!grtf}8T|Rw7+@PEJ92rQJ?I_(FtZMm}Q$(d0I-vSp-7CC8tim;^ z)Osi^8^1fS2nyF!02%g4buNyM?CsWQ5j?tdzDG&iv{bjKf7Pb{ZggU6{dr`APVE)- zz%j8!|4c7|rrKFN3=U;ry#~9ydx+tSLmQ{UG`+xs@i)!VbjXazL3l+)IU&xp4Z! z!4?ynGDL7|gu$9LdpkszpjD_`=tv!T?n_lR`QoP?fyxBVvh{PB)_IWgIPun=hkgbA zx|d>F^Vxk8eja!N^U!IxJ|W6&-%)?p({WJ3hz3>fbP%X-)0ix1V^v?S_c}yi#^{LA z_x%i&vG0aqBWD9Fdf&RPblZSIe~`c?AZsm#cdk;D#uYpet#a?CvIh6RQ*TY+rKP#d{0=#N&nC`Q`8 zV{hCbNEF>M)1}ex!9sED54b2Sr^HkSIJ6XZ|DxMKSSiPXrF63pDey3u0L2E^8?4D# z7|HP9VO;@Mr6Ber)^%wE3rzE#nZ8e8PK=vGm#(EmI2r0Lm#!VVv6d}U9?SQeH5Ol< z_amK{!61+8$yttRp>WL_i3E-*SxqgG8=6iGWI`3b&T(>JOmvy}+;^k1n-Ut239qj2 zalx&GGVcx28Pz@NG(V_23c{kz`p}mcF%mP)olu==&H92Y#g`vclPjFH*MQi5nlIkZMf{yjX@hfS$U7Oxz5Zf$Y4S7uCE^0wf)86nB=<=(&NNStq`A?CW-N-;Ho{?=YHk=UUl)ndTjG^tfxzV0dPnf%AmJ8 z@3QihAI1wtNvQJ6)Q5@#P2_m@+0wd@3EeniTb_^%S;FhkUxFW!L}>D&hC0O&S`#%7 zH?xA)>Pf1B2t`F`H@z5j7+Bz_G{2O-^{r(c)}~|ikDs1Zp5*?N9BquIE;k>a$FV6v z4uf*EUO!5yFsJam8l~0YzGt%a6t8L4hZ6c`ea5yY41WJm_kZNrY@)&b0MJj&wfXB$ R^ZgFm%*e{H?#}&Z{{wmt^bi05 literal 0 HcmV?d00001 diff --git a/assets/cn/handler/SUBMARINE_MOVE_ENTER.png b/assets/cn/handler/SUBMARINE_MOVE_ENTER.png new file mode 100644 index 0000000000000000000000000000000000000000..b331b54ade945d86f28c32a52fdb7ab1e976b05f GIT binary patch literal 10802 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 z%0AgVmmZRm|L@h7Vsmb}cYlx?v6;hlD`fwo=M0#4{);tdpN=;uBZ`fMo`ffNa~${| z8VhQzn3FseQ>8~Eb{OyoPKEk0s*Y!M=9wnKJ*2NI$vY90L7=43(W|QHhDBR52kxHg zQK{#KiFH)6n}DMAx5O7xeFX`^>CJvmN)lBF+uihNAiRE*Zbty6AKmM_4^u)A;w!7l+>0~fkoKzO zClRag?+kuEN z)S~P?ACR7Kmdi#%Q?=(G)K5{%g1zK9qlD{?vwr2>0TJ0)n#eCEK8Fw_z0f?5^M}dV zLXmI1b4lRl^^Hy?oFTsI(wwh{V(Q()l#L>mtMrrrECLTEbn02U=PUUJmkD-!hrcs7 zs2{0#4>|2hL=53U&0hbKl%?-tqp4^Fx=D-9~dR;N>rhnAYZWx_9 zb|j;B&=^VyEUNwbjfRt&pqM|~%9^}2HFJj!{t9bq&b91I`~p~%A5_bJWq;}jQ@@6} zqo0<7!I1q;bE|X5%Gav8H1n=;UAM{cwNNR_iNzuVzyoY|Ws(8S9qCW1}ZmNDORqLK= z-R`VqfyrQVGdLbAgbko!FrCJChK`@}PD=e>R^7%`S%J}Je6K;XHhhT9-AAqc|Li3= zi@MVE6FAc32gzG*_`?VFdY;(iRE~v|jd7*LG+KX@F%A)9iYsAd(H-cTl&c94SfNLu zcW@9|wk2y!Cpo3Dzw^AadE%2Zv8Fg=Uob&9`6V59?{NC^VYQb`-5>7{WmY>V%$Y)c zneC0nrz-_IZ&OMM%qu>Tl9Y~)cVdq4v%0YK}4GE8O?$f!w03BJoHnco@G2by0SgTq;mjEzbWZ;&%(kTYQSPotO;tUOIDI=X*Zq$fvctEfO!xy)swy{xPZ z!pFs<&tjh$VlsB`Sfe@0bq+ot;G!Q?&7fxYfmo->k%X|CvTpZ@qs-OIFtg4)ZC$KYVdR8?eh0`!Z%F zqk8?IdA3UXCuPbz99cgl-Ca09?R0Ea^>Lz+I|6V44%>GGw!?JX!& zq59G+CuBNuCFuj44!_EHB-uUQ#YG$+w;)of4SzAe~&0cbF&?HYTo6T5dXM0pJ zd-6HFYkZ<=J0b@_mCV`AdZkAkLOZokzmLxwrd?2l{rPx9zF7L4vi^Ld1wFBhiUM(HRv)wBq~0BL0_frszEFGmoYfY zNZm<&+;^j9;Y0tIEBA{2)I9Gm%dTfTUOE1BQ#qm8A3k&7BJBzN2-@?Kblf&MN4PA}bE(*#RtZ$o&x^CJ<@zO&1D{Av$go~C{;$yXPL@To%i7pli zZVhw_eI*n}r|&mbJyJv7)y*k~87|F#aE~$rY2gu@f-KqT8{$MO(WX93H|KIJMa1!s z+-RZyKV-^pGvUYY=AQK5(y4KL3Zq38FPPN)XE$5*r<|2C-sN83BP(FdOa-yZOJ?!z z&cl9)Z>6jGd`f$h{4jiYz2{>8g4&A!SDD1u0^SiZ?omSP@kd4PdrC`7J=p_j|68$A z`Hqg3yN6=R2cJXWNs&0a!X9P?CZgu#Qj4QYBYF4`o;?-5#v5`VGDR^lAybA{ecJ(2N5`# zhw)HFYf`B;1UcK*RVP(Zdn?<6%(|wVpjz?0x;h0K*I#ZXbEU$5tH%_#x=#$Ls$buV zHj83gQ20J1f;jb&${9z zUu$={X-*y|ro+0}u|`oK zLR9xO(J0nDtj%~AO_qzpxI$2AlFivW(NCM#!j$p)$*Nds6Ym#?D7D>DbCaARU78EG ztgAd&UjGUnkTy_Pi3OEY?b%o)j!)4^HE?r;DOzKmZZ6ax8)Qc=_SdK?QpeBIJ%p&l z^RS3NZverfQ1SEzFBl&Ma?|A5Sv=!G){g5NcuBbV@CI`i-*NAn4Kq(o6{B+6J@fLV zhN=k57fC+in+4nB-n-{dUZHvO4GD@3MSW9CJLVH+#_g++bV;7$mb1SCeb-0TaAp}d zEjq95WVvO6M-#Pq_igbC5aap-Q&Trlwaq9#|6w)1;b3aO*TX4_FCjtMXY2Ti8$K9* zMWc8--ZXQWuyYKubadyPSN`ZfV6Xf09yBQJF|usm&gst?1)W!dK=_EDrwrn4CGUMt8-O%RhV*f1WvB{8bgYHRrBC+nQGg9M`Y-T znwh2FC|$UeKcL&k6(K=T>#f{&w%cm7=ElSHXVnws4Vd*?V*UpnHIv$L_TwXn@gpHh zo3U~85bKmxl%}A0wLht!&wP2uFO{_Nd%%uLQQZ^11J$xa+L6X-8wP__mkikzKw9(T zaQ8sv!ro1@4LucT!ZAz*x8Ijw(zm{5;PfpQ~uJ{#!agRP1yY{yDZ zV*M(|;)B*C8!UDyWTUm|ey7A74#(|n-qE*2Zo7fAgtDfWA(!oztaU)bpJKg+d z_M(|vM-@*5zcaq-*db89t0u!3Up4%Ku=R%S4%_ohz2L8ZC1LqN*=#H@!gkSi#@~Xv zC-r1RSP%qZH-LiYALt3H|4K{?SZSY<-zc#R{0AK;iloer_N4R3dFcsaHI2g@xSzR` zqAxZ|q#5$C7`PEMjPlYdN&N15dDpmFA!HPh?_)T9^TMl_K0vnR$)bEoW$bXjdt=Ij=)@EK2 zlyPbQ5SbgNp*}&^=#G?IiiU#awh$ufV`{4T3roM1QKD}hF6n=>$rbZPD}vl$zo`oU zXByvVXg5|~w>x+-Pm3-rWu&Aw<^z$E88@b0z?h*mQ0IA+vc)N7p*3gG-@0-w#cV7b zIN!DY($4(+y1NRqIqSum_fCq%@?%b|SIQiU(c}i0)U(|7@0+99U)%ce**h#BCR2A+ z35_&eu8XBixF&tND;vL^V`uxb&*S(fS)=(#Xgg5|`>kFmd)`qY&|Q2hK@TfH@H)&+ zwAjN{Q8C9ewEgJ|>^sJeSQ_tZhoTM*Kjtybz-i&uQ4z|4-%_CY>yUUTEjs77F zGaqI#&=NGj9vXwuT-h*!Uap}t+AY&)Ki`$dJ1`X06mli~U?EP8W~g?9rGCF^L-;r# zd;D%m;AVjHji5WHU;d$fJmacUGZ-BDrS%$m2W~r!`hw*$W`8w)hy47;e9FeumG`vu z!2L{8p#Lg`&hWj_wR1G0rPHwA{x*fnqKEbzKkP=>CYT=O1iX6UrU%FF9Kwutj0Cg$ z+UWHBgGxCXgCHrtgCR3+UgnF0}| zDL5#&MVS2ga%*gIL&6XRbWXx92gk5*;k0oc4ZtZT-GKTd2@QyI!T}Y#CnQ1(}WK5cPAR n=lFl^KLq|C5LjjYdCUjALl;SeF_6?w9(b&-2QGT}^6mctel69z literal 0 HcmV?d00001 diff --git a/assets/en/handler/SUBMARINE_MOVE_ENTER.png b/assets/en/handler/SUBMARINE_MOVE_ENTER.png new file mode 100644 index 0000000000000000000000000000000000000000..b331b54ade945d86f28c32a52fdb7ab1e976b05f GIT binary patch literal 10802 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 z%0AgVmmZRm|L@h7Vsmb}cYlx?v6;hlD`fwo=M0#4{);tdpN=;uBZ`fMo`ffNa~${| z8VhQzn3FseQ>8~Eb{OyoPKEk0s*Y!M=9wnKJ*2NI$vY90L7=43(W|QHhDBR52kxHg zQK{#KiFH)6n}DMAx5O7xeFX`^>CJvmN)lBF+uihNAiRE*Zbty6AKmM_4^u)A;w!7l+>0~fkoKzO zClRag?+kuEN z)S~P?ACR7Kmdi#%Q?=(G)K5{%g1zK9qlD{?vwr2>0TJ0)n#eCEK8Fw_z0f?5^M}dV zLXmI1b4lRl^^Hy?oFTsI(wwh{V(Q()l#L>mtMrrrECLTEbn02U=PUUJmkD-!hrcs7 zs2{0#4>|2hL=53U&0hbKl%?-tqp4^Fx=D-9~dR;N>rhnAYZWx_9 zb|j;B&=^VyEUNwbjfRt&pqM|~%9^}2HFJj!{t9bq&b91I`~p~%A5_bJWq;}jQ@@6} zqo0<7!I1q;bE|X5%Gav8H1n=;UAM{cwNNR_iNzuVzyoY|Ws(8S9qCW1}ZmNDORqLK= z-R`VqfyrQVGdLbAgbko!FrCJChK`@}PD=e>R^7%`S%J}Je6K;XHhhT9-AAqc|Li3= zi@MVE6FAc32gzG*_`?VFdY;(iRE~v|jd7*LG+KX@F%A)9iYsAd(H-cTl&c94SfNLu zcW@9|wk2y!Cpo3Dzw^AadE%2Zv8Fg=Uob&9`6V59?{NC^VYQb`-5>7{WmY>V%$Y)c zneC0nrz-_IZ&OMM%qu>Tl9Y~)cVdq4v%0YK}4GE8O?$f!w03BJoHnco@G2by0SgTq;mjEzbWZ;&%(kTYQSPotO;tUOIDI=X*Zq$fvctEfO!xy)swy{xPZ z!pFs<&tjh$VlsB`Sfe@0bq+ot;G!Q?&7fxYfmo->k%X|CvTpZ@qs-OIFtg4)ZC$KYVdR8?eh0`!Z%F zqk8?IdA3UXCuPbz99cgl-Ca09?R0Ea^>Lz+I|6V44%>GGw!?JX!& zq59G+CuBNuCFuj44!_EHB-uUQ#YG$+w;)of4SzAe~&0cbF&?HYTo6T5dXM0pJ zd-6HFYkZ<=J0b@_mCV`AdZkAkLOZokzmLxwrd?2l{rPx9zF7L4vi^Ld1wFBhiUM(HRv)wBq~0BL0_frszEFGmoYfY zNZm<&+;^j9;Y0tIEBA{2)I9Gm%dTfTUOE1BQ#qm8A3k&7BJBzN2-@?Kblf&MN4PA}bE(*#RtZ$o&x^CJ<@zO&1D{Av$go~C{;$yXPL@To%i7pli zZVhw_eI*n}r|&mbJyJv7)y*k~87|F#aE~$rY2gu@f-KqT8{$MO(WX93H|KIJMa1!s z+-RZyKV-^pGvUYY=AQK5(y4KL3Zq38FPPN)XE$5*r<|2C-sN83BP(FdOa-yZOJ?!z z&cl9)Z>6jGd`f$h{4jiYz2{>8g4&A!SDD1u0^SiZ?omSP@kd4PdrC`7J=p_j|68$A z`Hqg3yN6=R2cJXWNs&0a!X9P?CZgu#Qj4QYBYF4`o;?-5#v5`VGDR^lAybA{ecJ(2N5`# zhw)HFYf`B;1UcK*RVP(Zdn?<6%(|wVpjz?0x;h0K*I#ZXbEU$5tH%_#x=#$Ls$buV zHj83gQ20J1f;jb&${9z zUu$={X-*y|ro+0}u|`oK zLR9xO(J0nDtj%~AO_qzpxI$2AlFivW(NCM#!j$p)$*Nds6Ym#?D7D>DbCaARU78EG ztgAd&UjGUnkTy_Pi3OEY?b%o)j!)4^HE?r;DOzKmZZ6ax8)Qc=_SdK?QpeBIJ%p&l z^RS3NZverfQ1SEzFBl&Ma?|A5Sv=!G){g5NcuBbV@CI`i-*NAn4Kq(o6{B+6J@fLV zhN=k57fC+in+4nB-n-{dUZHvO4GD@3MSW9CJLVH+#_g++bV;7$mb1SCeb-0TaAp}d zEjq95WVvO6M-#Pq_igbC5aap-Q&Trlwaq9#|6w)1;b3aO*TX4_FCjtMXY2Ti8$K9* zMWc8--ZXQWuyYKubadyPSN`ZfV6Xf09yBQJF|usm&gst?1)W!dK=_EDrwrn4CGUMt8-O%RhV*f1WvB{8bgYHRrBC+nQGg9M`Y-T znwh2FC|$UeKcL&k6(K=T>#f{&w%cm7=ElSHXVnws4Vd*?V*UpnHIv$L_TwXn@gpHh zo3U~85bKmxl%}A0wLht!&wP2uFO{_Nd%%uLQQZ^11J$xa+L6X-8wP__mkikzKw9(T zaQ8sv!ro1@4LucT!ZAz*x8Ijw(zm{5;PfpQ~uJ{#!agRP1yY{yDZ zV*M(|;)B*C8!UDyWTUm|ey7A74#(|n-qE*2Zo7fAgtDfWA(!oztaU)bpJKg+d z_M(|vM-@*5zcaq-*db89t0u!3Up4%Ku=R%S4%_ohz2L8ZC1LqN*=#H@!gkSi#@~Xv zC-r1RSP%qZH-LiYALt3H|4K{?SZSY<-zc#R{0AK;iloer_N4R3dFcsaHI2g@xSzR` zqAxZ|q#5$C7`PEMjPlYdN&N15dDpmFA!HPh?_)T9^TMl_K0vnR$)bEoW$bXjdt=Ij=)@EK2 zlyPbQ5SbgNp*}&^=#G?IiiU#awh$ufV`{4T3roM1QKD}hF6n=>$rbZPD}vl$zo`oU zXByvVXg5|~w>x+-Pm3-rWu&Aw<^z$E88@b0z?h*mQ0IA+vc)N7p*3gG-@0-w#cV7b zIN!DY($4(+y1NRqIqSum_fCq%@?%b|SIQiU(c}i0)U(|7@0+99U)%ce**h#BCR2A+ z35_&eu8XBixF&tND;vL^V`uxb&*S(fS)=(#Xgg5|`>kFmd)`qY&|Q2hK@TfH@H)&+ zwAjN{Q8C9ewEgJ|>^sJeSQ_tZhoTM*Kjtybz-i&uQ4z|4-%_CY>yUUTEjs77F zGaqI#&=NGj9vXwuT-h*!Uap}t+AY&)Ki`$dJ1`X06mli~U?EP8W~g?9rGCF^L-;r# zd;D%m;AVjHji5WHU;d$fJmacUGZ-BDrS%$m2W~r!`hw*$W`8w)hy47;e9FeumG`vu z!2L{8p#Lg`&hWj_wR1G0rPHwA{x*fnqKEbzKkP=>CYT=O1iX6UrU%FF9Kwutj0Cg$ z+UWHBgGxCXgCHrtgCR3+UgnF0}| zDL5#&MVS2ga%*gIL&6XRbWXx92gk5*;k0oc4ZtZT-GKTd2@QyI!T}Y#CnQ1(}WK5cPAR n=lFl^KLq|C5LjjYdCUjALl;SeF_6?w9(b&-2QGT}^6mctel69z literal 0 HcmV?d00001 diff --git a/assets/jp/handler/SUBMARINE_MOVE_ENTER.png b/assets/jp/handler/SUBMARINE_MOVE_ENTER.png new file mode 100644 index 0000000000000000000000000000000000000000..b331b54ade945d86f28c32a52fdb7ab1e976b05f GIT binary patch literal 10802 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 z%0A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 literal 0 HcmV?d00001 diff --git a/assets/tw/handler/SUBMARINE_MOVE_ENTER.png b/assets/tw/handler/SUBMARINE_MOVE_ENTER.png new file mode 100644 index 0000000000000000000000000000000000000000..b331b54ade945d86f28c32a52fdb7ab1e976b05f GIT binary patch literal 10802 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 z%0A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 literal 0 HcmV?d00001 diff --git a/module/handler/assets.py b/module/handler/assets.py index cded8f5a1..fe4ccad5a 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -70,6 +70,9 @@ STRATEGY_OPEN = Button(area={'cn': (1198, 411, 1269, 471), 'en': (1198, 410, 127 STRATEGY_OPENED = Button(area={'cn': (1176, 366, 1275, 393), 'en': (1176, 366, 1276, 393), 'jp': (1178, 367, 1273, 391), 'tw': (1176, 366, 1275, 392)}, color={'cn': (128, 155, 218), 'en': (108, 139, 210), 'jp': (156, 176, 223), 'tw': (126, 153, 218)}, button={'cn': (1060, 406, 1092, 485), 'en': (1060, 406, 1092, 485), 'jp': (1060, 406, 1092, 485), 'tw': (1060, 406, 1092, 485)}, file={'cn': './assets/cn/handler/STRATEGY_OPENED.png', 'en': './assets/en/handler/STRATEGY_OPENED.png', 'jp': './assets/jp/handler/STRATEGY_OPENED.png', 'tw': './assets/tw/handler/STRATEGY_OPENED.png'}) SUBMARINE_HUNT_OFF = Button(area={'cn': (1200, 415, 1262, 477), 'en': (1200, 415, 1262, 477), 'jp': (1200, 415, 1262, 477), 'tw': (1200, 415, 1262, 477)}, color={'cn': (125, 127, 132), 'en': (125, 127, 132), 'jp': (125, 127, 132), 'tw': (125, 127, 132)}, button={'cn': (1200, 415, 1262, 477), 'en': (1200, 415, 1262, 477), 'jp': (1200, 415, 1262, 477), 'tw': (1200, 415, 1262, 477)}, file={'cn': './assets/cn/handler/SUBMARINE_HUNT_OFF.png', 'en': './assets/en/handler/SUBMARINE_HUNT_OFF.png', 'jp': './assets/jp/handler/SUBMARINE_HUNT_OFF.png', 'tw': './assets/tw/handler/SUBMARINE_HUNT_OFF.png'}) SUBMARINE_HUNT_ON = Button(area={'cn': (1200, 415, 1262, 477), 'en': (1200, 415, 1262, 477), 'jp': (1200, 415, 1262, 477), 'tw': (1200, 415, 1262, 477)}, color={'cn': (124, 125, 132), 'en': (124, 125, 132), 'jp': (124, 125, 132), 'tw': (124, 125, 132)}, button={'cn': (1200, 415, 1262, 477), 'en': (1200, 415, 1262, 477), 'jp': (1200, 415, 1262, 477), 'tw': (1200, 415, 1262, 477)}, file={'cn': './assets/cn/handler/SUBMARINE_HUNT_ON.png', 'en': './assets/en/handler/SUBMARINE_HUNT_ON.png', 'jp': './assets/jp/handler/SUBMARINE_HUNT_ON.png', 'tw': './assets/tw/handler/SUBMARINE_HUNT_ON.png'}) +SUBMARINE_MOVE_CANCEL = Button(area={'cn': (891, 647, 1005, 673), 'en': (891, 647, 1005, 673), 'jp': (891, 647, 1005, 673), 'tw': (891, 647, 1005, 673)}, color={'cn': (219, 172, 167), 'en': (219, 172, 167), 'jp': (219, 172, 167), 'tw': (219, 172, 167)}, button={'cn': (891, 647, 1005, 673), 'en': (891, 647, 1005, 673), 'jp': (891, 647, 1005, 673), 'tw': (891, 647, 1005, 673)}, file={'cn': './assets/cn/handler/SUBMARINE_MOVE_CANCEL.png', 'en': './assets/cn/handler/SUBMARINE_MOVE_CANCEL.png', 'jp': './assets/cn/handler/SUBMARINE_MOVE_CANCEL.png', 'tw': './assets/cn/handler/SUBMARINE_MOVE_CANCEL.png'}) +SUBMARINE_MOVE_CONFIRM = Button(area={'cn': (1103, 646, 1218, 674), 'en': (1103, 646, 1218, 674), 'jp': (1103, 646, 1218, 674), 'tw': (1103, 646, 1218, 674)}, color={'cn': (157, 185, 222), 'en': (157, 185, 222), 'jp': (157, 185, 222), 'tw': (157, 185, 222)}, button={'cn': (1103, 646, 1218, 674), 'en': (1103, 646, 1218, 674), 'jp': (1103, 646, 1218, 674), 'tw': (1103, 646, 1218, 674)}, file={'cn': './assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png', 'en': './assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png', 'jp': './assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png', 'tw': './assets/cn/handler/SUBMARINE_MOVE_CONFIRM.png'}) +SUBMARINE_MOVE_ENTER = Button(area={'cn': (1109, 511, 1169, 571), 'en': (1109, 511, 1169, 571), 'jp': (1109, 511, 1169, 571), 'tw': (1109, 511, 1169, 571)}, color={'cn': (106, 107, 114), 'en': (106, 107, 114), 'jp': (106, 107, 114), 'tw': (106, 107, 114)}, button={'cn': (1109, 511, 1169, 571), 'en': (1109, 511, 1169, 571), 'jp': (1109, 511, 1169, 571), 'tw': (1109, 511, 1169, 571)}, file={'cn': './assets/cn/handler/SUBMARINE_MOVE_ENTER.png', 'en': './assets/en/handler/SUBMARINE_MOVE_ENTER.png', 'jp': './assets/jp/handler/SUBMARINE_MOVE_ENTER.png', 'tw': './assets/tw/handler/SUBMARINE_MOVE_ENTER.png'}) SUBMARINE_VIEW_OFF = Button(area={'cn': (1140, 435, 1170, 468), 'en': (1140, 435, 1170, 468), 'jp': (1140, 435, 1170, 468), 'tw': (1140, 435, 1170, 468)}, color={'cn': (156, 156, 158), 'en': (156, 156, 158), 'jp': (156, 156, 158), 'tw': (156, 156, 158)}, button={'cn': (1140, 435, 1170, 468), 'en': (1140, 435, 1170, 468), 'jp': (1140, 435, 1170, 468), 'tw': (1140, 435, 1170, 468)}, file={'cn': './assets/cn/handler/SUBMARINE_VIEW_OFF.png', 'en': './assets/en/handler/SUBMARINE_VIEW_OFF.png', 'jp': './assets/jp/handler/SUBMARINE_VIEW_OFF.png', 'tw': './assets/tw/handler/SUBMARINE_VIEW_OFF.png'}) SUBMARINE_VIEW_ON = Button(area={'cn': (1140, 435, 1170, 468), 'en': (1140, 435, 1170, 468), 'jp': (1140, 435, 1170, 468), 'tw': (1140, 435, 1170, 468)}, color={'cn': (177, 178, 179), 'en': (177, 178, 179), 'jp': (177, 178, 179), 'tw': (177, 178, 179)}, button={'cn': (1140, 435, 1170, 468), 'en': (1140, 435, 1170, 468), 'jp': (1140, 435, 1170, 468), 'tw': (1140, 435, 1170, 468)}, file={'cn': './assets/cn/handler/SUBMARINE_VIEW_ON.png', 'en': './assets/en/handler/SUBMARINE_VIEW_ON.png', 'jp': './assets/jp/handler/SUBMARINE_VIEW_ON.png', 'tw': './assets/tw/handler/SUBMARINE_VIEW_ON.png'}) USER_AGREEMENT_CONFIRM = Button(area={'cn': (709, 526, 742, 542), 'en': (709, 526, 742, 542), 'jp': (709, 526, 742, 542), 'tw': (709, 526, 742, 542)}, color={'cn': (151, 216, 243), 'en': (151, 216, 243), 'jp': (151, 216, 243), 'tw': (151, 216, 243)}, button={'cn': (709, 526, 742, 542), 'en': (709, 526, 742, 542), 'jp': (709, 526, 742, 542), 'tw': (709, 526, 742, 542)}, file={'cn': './assets/cn/handler/USER_AGREEMENT_CONFIRM.png', 'en': './assets/en/handler/USER_AGREEMENT_CONFIRM.png', 'jp': './assets/jp/handler/USER_AGREEMENT_CONFIRM.png', 'tw': './assets/tw/handler/USER_AGREEMENT_CONFIRM.png'}) diff --git a/module/handler/strategy.py b/module/handler/strategy.py index c1fc0f602..fa245943d 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -80,19 +80,11 @@ class StrategyHandler(InfoHandler): fleet_1_formation_fixed = False fleet_2_formation_fixed = False - def handle_opened_strategy_bar(self): - if self.appear_then_click(STRATEGY_OPENED, offset=120): - self.device.sleep(0.5) - return True - - return False - def strategy_open(self): logger.info('Strategy open') while 1: if self.appear(IN_MAP, interval=5) and not self.appear(STRATEGY_OPENED, offset=120): self.device.click(STRATEGY_OPEN) - self.device.sleep(0.5) if self.appear(STRATEGY_OPENED, offset=120): break @@ -103,7 +95,7 @@ class StrategyHandler(InfoHandler): logger.info('Strategy close') while 1: if self.appear_then_click(STRATEGY_OPENED, offset=120, interval=5): - self.device.sleep(0.5) + pass if not self.appear(STRATEGY_OPENED, offset=120): break @@ -174,3 +166,55 @@ class StrategyHandler(InfoHandler): logger.attr('Map_buff', buff) return buff + + def strategy_submarine_move_enter(self): + """ + Pages: + in: STRATEGY_OPENED, SUBMARINE_MOVE_ENTER + out: SUBMARINE_MOVE_CONFIRM + """ + logger.info('Submarine move enter') + while 1: + if self.appear(SUBMARINE_MOVE_ENTER, offset=120, interval=5): + self.device.click(SUBMARINE_MOVE_ENTER) + + if self.appear(SUBMARINE_MOVE_CONFIRM, offset=(20, 20)): + break + + self.device.screenshot() + + def strategy_submarine_move_confirm(self): + """ + Pages: + in: SUBMARINE_MOVE_CONFIRM + out: STRATEGY_OPENED, SUBMARINE_MOVE_ENTER + """ + logger.info('Submarine move confirm') + while 1: + if self.appear_then_click(SUBMARINE_MOVE_CONFIRM, offset=(20, 20), interval=5): + pass + if self.handle_popup_confirm('SUBMARINE_MOVE'): + pass + + if self.appear(SUBMARINE_MOVE_ENTER, offset=120): + break + + self.device.screenshot() + + def strategy_submarine_move_cancel(self): + """ + Pages: + in: SUBMARINE_MOVE_CONFIRM + out: STRATEGY_OPENED, SUBMARINE_MOVE_ENTER + """ + logger.info('Submarine move cancel') + while 1: + if self.appear_then_click(SUBMARINE_MOVE_CANCEL, offset=(20, 20), interval=5): + pass + if self.handle_popup_confirm('SUBMARINE_MOVE'): + pass + + if self.appear(SUBMARINE_MOVE_ENTER, offset=120): + break + + self.device.screenshot() diff --git a/module/map/fleet.py b/module/map/fleet.py index 708921106..ef416aef9 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -917,3 +917,85 @@ class Fleet(Camera, AmbushHandler): self.map_fleet_checked = False self.fleet_1_formation_fixed = False self.fleet_2_formation_fixed = False + + def _submarine_goto(self, location): + """ + Move submarine to given location. + + Args: + location (tuple, str, GridInfo): Destination. + + Returns: + bool: If submarine moved. + + Pages: + in: SUBMARINE_MOVE_CONFIRM + out: SUBMARINE_MOVE_CONFIRM + """ + location = location_ensure(location) + moved = True + while 1: + self.in_sight(location, sight=self._walk_sight) + self.focus_to_grid_center() + grid = self.convert_global_to_local(location) + grid.__str__ = location + + self.device.click(grid) + arrived = False + # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. + arrive_timer = Timer(0.1, count=2) + # If nothing happens, click again. + walk_timeout = Timer(2, count=6).start() + + while 1: + self.device.screenshot() + self.view.update(image=self.device.image) + + # Arrive + arrive_checker = grid.predict_submarine_move() + if grid.predict_submarine() or (walk_timeout.reached() and grid.predict_fleet()): + arrive_checker = True + moved = False + if arrive_checker: + if not arrive_timer.started(): + logger.info(f'Arrive {location2node(location)}') + arrive_timer.start() + if not arrive_timer.reached(): + continue + logger.info(f'Submarine arrive {location2node(location)} confirm.') + if not moved: + logger.info(f'Submarine already at {location2node(location)}') + arrived = True + break + + # End + if walk_timeout.reached(): + logger.warning('Walk timeout. Retrying.') + self.predict() + self.ensure_edge_insight(skip_first_update=False) + break + + # End + if arrived: + break + + return moved + + def submarine_goto(self, location): + """ + Open strategy, move submarine to given location, close strategy. + + Args: + location (tuple, str, GridInfo): Destination. + + Pages: + in: IN_MAP + out: IN_MAP + """ + self.strategy_open() + self.strategy_submarine_move_enter() + if self._submarine_goto(location): + self.strategy_submarine_move_confirm() + else: + self.strategy_submarine_move_cancel() + self.strategy_close() diff --git a/module/map_detection/grid_predictor.py b/module/map_detection/grid_predictor.py index 59c635b2f..f686ce00b 100644 --- a/module/map_detection/grid_predictor.py +++ b/module/map_detection/grid_predictor.py @@ -284,6 +284,10 @@ class GridPredictor: return False + def predict_submarine_move(self): + # Detect the orange arrow in submarine movement mode. + return self.relative_rgb_count((-0.5, -1, 0.5, 0), color=(231, 138, 49), shape=(60, 60)) > 200 + @cached_property def _image_similar_piece(self): return rgb2gray(self.relative_crop(area=(-0.5, -0.5, 0.5, 0.5), shape=(60, 60))) From 1730b9f395983ca2ff7c1a906e880532d2d7a6fe Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sun, 9 Jan 2022 20:31:57 +0800 Subject: [PATCH 02/18] Add: Configs to call submarine only at BOSS (#864) --- config/template.json | 29 +++++--- module/config/argument/args.json | 99 ++++++++++++++++++++++++++++ module/config/argument/argument.yaml | 5 +- module/config/config_generated.py | 3 +- module/config/i18n/en-US.json | 17 +++-- module/config/i18n/ja-JP.json | 9 +++ module/config/i18n/zh-CN.json | 9 +++ module/config/i18n/zh-TW.json | 9 +++ 8 files changed, 164 insertions(+), 16 deletions(-) diff --git a/config/template.json b/config/template.json index e8aa846d7..226d91a42 100644 --- a/config/template.json +++ b/config/template.json @@ -93,7 +93,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -165,7 +166,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -356,7 +358,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -430,7 +433,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -569,7 +573,7 @@ "UseCube": "only_05_hour", "UseCoin": "always_use", "UsePart": "always_use", - "PresetFilter": "series_4", + "PresetFilter": "series_4_blueprint_tenrai", "CustomFilter": "S4-Q0.5 > Q-0.5 > S4-DR0.5 > S4-PRY0.5 > DR-0.5 > PRY-0.5\n> S4-Q1 > S4-Q2\n> S4-DR2.5 > S4-G1.5\n> S4-Q4 > S4-H0.5 > S4-G4\n> S4-PRY2.5 > S4-G2.5\n> reset > S4-H1 > shortest" } }, @@ -861,7 +865,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -934,7 +939,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -1007,7 +1013,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -1076,7 +1083,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, @@ -1190,7 +1198,8 @@ "Submarine": { "Fleet": 0, "Mode": "do_not_use", - "AutoSearchMode": "sub_standby" + "AutoSearchMode": "sub_standby", + "DistanceToBoss": "2_grid_to_boss" }, "Emotion": { "CalculateEmotion": true, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index d8b887a37..fe39af5d6 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -407,6 +407,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -417,6 +418,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -806,6 +817,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -816,6 +828,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -1703,6 +1725,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -1713,6 +1736,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -2077,6 +2110,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -2087,6 +2121,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -3861,6 +3905,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -3871,6 +3916,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -4259,6 +4314,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -4269,6 +4325,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -4657,6 +4723,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -4667,6 +4734,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -5045,6 +5122,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -5055,6 +5133,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { @@ -5590,6 +5678,7 @@ "option": [ "do_not_use", "hunt_only", + "boss_only", "every_combat" ] }, @@ -5600,6 +5689,16 @@ "sub_standby", "sub_auto_call" ] + }, + "DistanceToBoss": { + "type": "select", + "value": "2_grid_to_boss", + "option": [ + "to_boss_position", + "1_grid_to_boss", + "2_grid_to_boss", + "use_U522_skill" + ] } }, "Emotion": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index adb74d001..2c07cc759 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -120,10 +120,13 @@ Submarine: option: [0, 1, 2] Mode: value: do_not_use - option: [do_not_use, hunt_only, every_combat] + option: [do_not_use, hunt_only, boss_only, every_combat] AutoSearchMode: value: sub_standby option: [ sub_standby, sub_auto_call ] + DistanceToBoss: + value: '2_grid_to_boss' + option: [to_boss_position, 1_grid_to_boss, 2_grid_to_boss, use_U522_skill] Emotion: CalculateEmotion: true IgnoreLowEmotionWarn: false diff --git a/module/config/config_generated.py b/module/config/config_generated.py index a2085d166..5dabc6259 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -87,8 +87,9 @@ class GeneratedConfig: # Group `Submarine` Submarine_Fleet = 0 # 0, 1, 2 - Submarine_Mode = 'do_not_use' # do_not_use, hunt_only, every_combat + Submarine_Mode = 'do_not_use' # do_not_use, hunt_only, boss_only, every_combat Submarine_AutoSearchMode = 'sub_standby' # sub_standby, sub_auto_call + Submarine_DistanceToBoss = '2_grid_to_boss' # to_boss_position, 1_grid_to_boss, 2_grid_to_boss, use_U522_skill # Group `Emotion` Emotion_CalculateEmotion = True diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 2fd21f018..38fc715a8 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -635,22 +635,31 @@ "Fleet": { "name": "Submarine Fleet Number", "help": "", - "0": "do_not_use", + "0": "Don't Use", "1": "1", "2": "2" }, "Mode": { "name": "Submarine Mode", "help": "", - "do_not_use": "do_not_use", - "hunt_only": "hunt_only", - "every_combat": "every_combat" + "do_not_use": "Don't Use", + "hunt_only": "Hunt Only", + "boss_only": "BOSS Only", + "every_combat": "Every Combat" }, "AutoSearchMode": { "name": "Submarine Roles in Auto Search", "help": "Effective only for auto search", "sub_standby": "Standby", "sub_auto_call": "Auto Call" + }, + "DistanceToBoss": { + "name": "Before BOSS battle move the submarine near BOSS", + "help": "Effective only if \"Submarine Mode\" is \"BOSS Only\"\nSelecting \"X Grids To BOSS\" needs to ensure that the submarine hunting range can cover the BOSS. Distance is calculated using the Manhattan Distance. Selecting \"Use U522 Skill\" requires U522 in the submarine fleet.", + "to_boss_position": "To BOSS Location", + "1_grid_to_boss": "1 Grid To BOSS", + "2_grid_to_boss": "2 Grids To BOSS", + "use_U522_skill": "Use U522 Skill" } }, "Emotion": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index f340b0a77..dcbd47d2b 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -644,6 +644,7 @@ "help": "Submarine.Mode.help", "do_not_use": "do_not_use", "hunt_only": "hunt_only", + "boss_only": "boss_only", "every_combat": "every_combat" }, "AutoSearchMode": { @@ -651,6 +652,14 @@ "help": "Submarine.AutoSearchMode.help", "sub_standby": "sub_standby", "sub_auto_call": "sub_auto_call" + }, + "DistanceToBoss": { + "name": "Submarine.DistanceToBoss.name", + "help": "Submarine.DistanceToBoss.help", + "to_boss_position": "to_boss_position", + "1_grid_to_boss": "1_grid_to_boss", + "2_grid_to_boss": "2_grid_to_boss", + "use_U522_skill": "use_U522_skill" } }, "Emotion": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 588c80342..c80a17a22 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -644,6 +644,7 @@ "help": "", "do_not_use": "不使用", "hunt_only": "仅狩猎", + "boss_only": "仅BOSS战", "every_combat": "每战出击" }, "AutoSearchMode": { @@ -651,6 +652,14 @@ "help": "自律时生效", "sub_standby": "待机", "sub_auto_call": "自动召唤潜艇" + }, + "DistanceToBoss": { + "name": "BOSS战前将潜艇移动到BOSS附近", + "help": "仅在\"潜艇出击方案\"为\"仅BOSS战\"时生效\n选择\"距离BOSS X格\"需要保证潜艇狩猎范围能覆盖到BOSS,距离使用曼哈顿距离计算,选择\"使用U522技能\"需要潜艇队伍里有U522", + "to_boss_position": "至 BOSS 所在位置", + "1_grid_to_boss": "距离 BOSS 1 格", + "2_grid_to_boss": "距离 BOSS 2 格", + "use_U522_skill": "使用 U522 技能" } }, "Emotion": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 39c2e9824..e18dcd3e9 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -644,6 +644,7 @@ "help": "", "do_not_use": "不使用", "hunt_only": "僅狩獵", + "boss_only": "僅BOSS", "every_combat": "每戰出擊" }, "AutoSearchMode": { @@ -651,6 +652,14 @@ "help": "自律尋敵下生效", "sub_standby": "待機", "sub_auto_call": "自動出擊" + }, + "DistanceToBoss": { + "name": "BOSS戰前將潛艇移動到BOSS附近", + "help": "僅在\"潛艇出擊方案\"為\"僅BOSS戰\"時生效\n選擇\"距離BOSS X格\"需要保證潛艇狩獵範圍能覆蓋到BOSS,距離使用曼哈頓距離計算,選擇\"使用U522技能\"需要潛艇隊伍裡有U522", + "to_boss_position": "至 BOSS 所在位置", + "1_grid_to_boss": "距離 BOSS 1 格", + "2_grid_to_boss": "距離 BOSS 2 格", + "use_U522_skill": "使用 U522 技能" } }, "Emotion": { From 487d4ed9e8ceed8ea68ccb87d5be43f213a58bd8 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sun, 9 Jan 2022 20:55:36 +0800 Subject: [PATCH 03/18] Fix: Force to disable auto submarine call if only calling submarine at boss --- module/handler/fast_forward.py | 24 ++++++++++++++++++++++++ module/map/map_fleet_preparation.py | 1 - module/map/map_operation.py | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/module/handler/fast_forward.py b/module/handler/fast_forward.py index 4ddc9f090..e5461d9c1 100644 --- a/module/handler/fast_forward.py +++ b/module/handler/fast_forward.py @@ -53,6 +53,7 @@ class FastForwardHandler(AutoSearchHandler): 'SP1 > SP2 > SP3 > SP4', 'T1 > T2 > T3 > T4', ] + map_fleet_checked = False def map_get_info(self): """ @@ -179,12 +180,35 @@ class FastForwardHandler(AutoSearchHandler): if not self.map_is_auto_search: return False + logger.info('Auto search setting') self.fleet_preparation_sidebar_ensure(3) self.auto_search_setting_ensure(self.config.Fleet_AutoSearchFleetOrder) if self.config.SUBMARINE: self.auto_search_setting_ensure(self.config.Submarine_AutoSearchMode) return True + @property + def is_call_submarine_at_boss(self): + return self.config.SUBMARINE and self.config.Submarine_Mode == 'boss_only' + + def handle_auto_submarine_call_disable(self): + """ + Returns: + bool: If changed + + Pages: + in: FLEET_PREPARATION + """ + if self.map_fleet_checked: + return False + if not self.is_call_submarine_at_boss: + return False + + logger.info('Disable auto submarine call') + self.fleet_preparation_sidebar_ensure(3) + self.auto_search_setting_ensure('sub_standby') + return True + def handle_auto_search_continue(self): """ Override AutoSearchHandler definition diff --git a/module/map/map_fleet_preparation.py b/module/map/map_fleet_preparation.py index 492289e81..b1ed202f5 100644 --- a/module/map/map_fleet_preparation.py +++ b/module/map/map_fleet_preparation.py @@ -288,5 +288,4 @@ class FleetPreparation(ModuleBase): else: submarine.clear() - self.map_fleet_checked = True return True diff --git a/module/map/map_operation.py b/module/map/map_operation.py index b4e709407..a41de6b3c 100644 --- a/module/map/map_operation.py +++ b/module/map/map_operation.py @@ -156,7 +156,9 @@ class MapOperation(MysteryHandler, FleetPreparation, Retirement, FastForwardHand if mode == 'normal' or mode == 'hard': self.handle_2x_book_setting(mode='prep') self.fleet_preparation() + self.handle_auto_submarine_call_disable() self.handle_auto_search_setting() + self.map_fleet_checked = True self.device.click(FLEET_PREPARATION) fleet_click += 1 fleet_timer.reset() From 1b5ecb21acceaa1851d46071d4fe3d8262271f98 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Sun, 9 Jan 2022 23:10:09 +0800 Subject: [PATCH 04/18] Opt: A revert of "Fix: Handle submarine view icon bug" Finally, that bug was fixed by game devs after 17 months. --- module/handler/strategy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/handler/strategy.py b/module/handler/strategy.py index fa245943d..7d17263d6 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -71,7 +71,7 @@ class SwitchWithHandler(Switch): changed = True -submarine_view = SwitchWithHandler('Submarine_view', offset=120) +submarine_view = Switch('Submarine_view', offset=120) submarine_view.add_status('on', check_button=SUBMARINE_VIEW_ON) submarine_view.add_status('off', check_button=SUBMARINE_VIEW_OFF) @@ -111,12 +111,14 @@ class StrategyHandler(InfoHandler): """ logger.info(f'Strategy set: formation={formation_index}, submarine_view={sub_view}, submarine_hunt={sub_hunt}') self.strategy_open() - self.device.screenshot() formation.set(str(formation_index), main=self) # Disable this until the icon bug of submarine zone is fixed # And don't enable MAP_HAS_DYNAMIC_RED_BORDER when using submarine + # Submarine view check is back again, see SwitchWithHandler. + + # Don't know when but the game bug was fixed, remove the use of SwitchWithHandler if submarine_view.appear(main=self): submarine_view.set('on' if sub_view else 'off', main=self) if submarine_hunt.appear(main=self): From 27bc8b38435e977fe206d32e1d1112f10f5104e2 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:33:30 +0800 Subject: [PATCH 05/18] Add: Detecting and moving submarines --- campaign/campaign_hard/campaign_hard.py | 2 +- module/map/fleet.py | 102 +++++++++++++++++++++++- module/map/map.py | 1 + module/map/map_base.py | 5 +- module/map_detection/grid_info.py | 31 ++++--- 5 files changed, 125 insertions(+), 16 deletions(-) diff --git a/campaign/campaign_hard/campaign_hard.py b/campaign/campaign_hard/campaign_hard.py index ce6ec2c09..785ceddb7 100644 --- a/campaign/campaign_hard/campaign_hard.py +++ b/campaign/campaign_hard/campaign_hard.py @@ -39,7 +39,7 @@ class Campaign(CampaignBase, HardEquipment): # def fleet_preparation(self): # self.equipment_take_on() - def _expected_combat_end(self, expected): + def _expected_end(self, expected): return 'in_stage' def clear_boss(self): diff --git a/module/map/fleet.py b/module/map/fleet.py index ef416aef9..e4f3ed782 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -15,6 +15,7 @@ from module.map.utils import match_movable class Fleet(Camera, AmbushHandler): fleet_1_location = () fleet_2_location = () + fleet_submarine_location = () battle_count = 0 mystery_count = 0 siren_count = 0 @@ -42,6 +43,14 @@ class Fleet(Camera, AmbushHandler): def fleet_2(self, value): self.fleet_2_location = value + @property + def fleet_submarine(self): + return self + + @fleet_submarine.setter + def fleet_submarine(self, value): + self.fleet_submarine_location = value + @property def fleet_current(self): if self.fleet_current_index == 2: @@ -288,7 +297,11 @@ class Fleet(Camera, AmbushHandler): if self.handle_combat_low_emotion(): walk_timeout.reset() if self.combat_appear(): - self.combat(expected_end=self._expected_combat_end(expected), fleet_index=self.fleet_show_index) + self.combat( + expected_end=self._expected_end(expected), + fleet_index=self.fleet_show_index, + submarine_mode=self._submarine_mode(expected) + ) self.hp_get() self.lv_get(after_battle=True) arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False @@ -475,6 +488,9 @@ class Fleet(Camera, AmbushHandler): fleets.append(text) logger.info(' '.join(fleets)) + def show_submarine(self): + logger.info(f'Submarine: {location2node(self.fleet_submarine_location)}') + def full_scan(self, queue=None, must_scan=None, mode='normal'): super().full_scan( queue=queue, must_scan=must_scan, battle_count=self.battle_count, mystery_count=self.mystery_count, @@ -685,6 +701,42 @@ class Fleet(Camera, AmbushHandler): self.show_fleet() return self.fleet_current + def find_all_submarines(self): + logger.hr('Find all submarines') + queue = self.map.select(is_submarine_spawn_point=True) + while queue: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0], sight=(-2, -1, 2, -1)) + grid = self.convert_global_to_local(queue[0]) + if grid.predict_submarine(): + self.fleet_submarine = queue[0].location + break + queue = queue[1:] + + def find_submarine(self): + if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): + return False + + fleets = self.map.select(is_submarine=True) + count = fleets.count + if count == 1: + self.fleet_submarine = fleets[0].location + elif count == 0: + logger.warning('No submarine found') + self.find_all_submarines() + else: + logger.warning('Too many submarines: %s.' % str(fleets)) + self.find_all_submarines() + + if not len(self.fleet_submarine_location): + logger.warning('Unable to find submarine, assume it is at map center') + shape = self.map.shape + center = (shape[0] // 2, shape[1] // 2) + self.fleet_submarine = self.map.select(is_land=False).sort_by_camera_distance(center)[0] + + self.show_submarine() + return self.fleet_submarine_location + def map_init(self, map_): """ This method should be called after entering a map and before doing any operations. @@ -706,6 +758,7 @@ class Fleet(Camera, AmbushHandler): """ self.fleet_1_location = () self.fleet_2_location = () + self.fleet_submarine_location = () self.fleet_current_index = 1 self.battle_count = 0 self.mystery_count = 0 @@ -745,6 +798,7 @@ class Fleet(Camera, AmbushHandler): self.handle_info_bar() # The info_bar which shows "Changed to fleet 2", will block the ammo icon self.full_scan(must_scan=self.map.camera_data_spawn_point) self.find_current_fleet() + self.find_submarine() self.find_path_initial() self.map.show_cost() self.round_reset() @@ -760,7 +814,7 @@ class Fleet(Camera, AmbushHandler): return True - def _expected_combat_end(self, expected): + def _expected_end(self, expected): for data in self.map.spawn_data: if data.get('battle') == self.battle_count and 'boss' in expected: return 'in_stage' @@ -775,6 +829,15 @@ class Fleet(Camera, AmbushHandler): return None + def _submarine_mode(self, expected): + if self.is_call_submarine_at_boss: + if 'boss' in expected: + return 'every_combat' + else: + return 'do_not_use' + else: + return None + def fleet_at(self, grid, fleet=None): """ Args: @@ -999,3 +1062,38 @@ class Fleet(Camera, AmbushHandler): else: self.strategy_submarine_move_cancel() self.strategy_close() + + def submarine_move_near_boss(self, boss): + if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): + return False + + boss = location_ensure(boss) + logger.info(f'Move submarine near {location2node(boss)}') + + self.map.find_path_initial(self.fleet_submarine_location, has_ambush=False, has_enemy=False) + self.map.show_cost() + + def get_location(distance=2): + grids = self.map.select(is_land=False).filter( + lambda grid: np.sum(np.abs(np.subtract(grid.location, boss))) <= distance) + if grids: + return grids.sort('cost')[0].location + elif distance > 0: + logger.info(f'Unable to find a grid near boss in distance {distance}, fallback to {distance - 1}') + return get_location(distance - 1) + else: + logger.warning(f'Unable to find a grid near boss in distance {distance}, return boss position') + return boss + + distance_dict = { + 'to_boss_position': 0, + '1_grid_to_boss': 1, + '2_grid_to_boss': 2 + } + logger.attr('Distance to boss', self.config.Submarine_DistanceToBoss) + near = get_location(distance_dict.get(self.config.Submarine_DistanceToBoss, 0)) + + self.find_path_initial() + + logger.info(f'Move submarine to {location2node(near)}') + self.submarine_goto(near) diff --git a/module/map/map.py b/module/map/map.py index 47f502a68..bbd7ecebe 100644 --- a/module/map/map.py +++ b/module/map/map.py @@ -325,6 +325,7 @@ class Map(Fleet): logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) if grids: + self.submarine_move_near_boss(grids[0]) logger.hr('Clear BOSS') grids = grids.sort('weight', 'cost') logger.info('Grids: %s' % str(grids)) diff --git a/module/map/map_base.py b/module/map/map_base.py index ba0b5f220..937b76b7c 100644 --- a/module/map/map_base.py +++ b/module/map/map_base.py @@ -487,11 +487,12 @@ class CampaignMap: range(self.shape[0] + 1)]) logger.info(text) - def find_path_initial(self, location, has_ambush=True): + def find_path_initial(self, location, has_ambush=True, has_enemy=True): """ Args: location (tuple(int)): Grid location has_ambush (bool): MAP_HAS_AMBUSH + has_enemy (bool): False if only sea and land are considered """ location = location_ensure(location) ambush_cost = 10 if has_ambush else 1 @@ -519,7 +520,7 @@ class CampaignMap: elif cost == arr.cost: if abs(arr.location[0] - grid.location[0]) == 1: arr.connection = grid.location - if arr.is_sea: + if arr.is_sea or not has_enemy: new.add(arr) if len(new) == len(visited): break diff --git a/module/map_detection/grid_info.py b/module/map_detection/grid_info.py index 4a27ea38c..5360d07cf 100644 --- a/module/map_detection/grid_info.py +++ b/module/map_detection/grid_info.py @@ -10,23 +10,24 @@ class GridInfo: which includes boss point, enemy spawn point. A grid contains these unchangeable properties which can known from WIKI. - | print_name | property_name | description | - |------------|----------------|-------------------------| - | ++ | is_land | fleet can't go to land | - | -- | is_sea | sea | - | __ | | submarine spawn point | - | SP | is_spawn_point | fleet may spawns here | - | ME | may_enemy | enemy may spawns here | - | MB | may_boss | boss may spawns here | - | MM | may_mystery | mystery may spawns here | - | MA | may_ammo | fleet can get ammo here | - | MS | may_siren | Siren/Elite enemy spawn | + | print_name | property_name | description | + |------------|--------------------------|-------------------------| + | ++ | is_land | fleet can't go to land | + | -- | is_sea | sea | + | __ | is_submarine_spawn_point | submarine spawn point | + | SP | is_spawn_point | fleet may spawns here | + | ME | may_enemy | enemy may spawns here | + | MB | may_boss | boss may spawns here | + | MM | may_mystery | mystery may spawns here | + | MA | may_ammo | fleet can get ammo here | + | MS | may_siren | Siren/Elite enemy spawn | """ is_os = False # is_sea -- is_land = False # ++ is_spawn_point = False # SP + is_submarine_spawn_point = False # __ may_enemy = False # ME may_boss = False # MB @@ -77,6 +78,7 @@ class GridInfo: dic = { '++': 'is_land', 'SP': 'is_spawn_point', + '__': 'is_submarine_spawn_point', 'ME': 'may_enemy', 'MB': 'may_boss', 'MM': 'may_mystery', @@ -176,6 +178,13 @@ class GridInfo: Returns: bool: If success. """ + # Submarines can be anywhere, so no success/failure in merging info + # But expects submarines at spawn points to be found at the beginning + if info.is_submarine: + if self.is_submarine_spawn_point: + self.is_submarine = True + else: + pass if info.is_caught_by_siren: if self.is_sea: self.is_fleet = True From 84f5e86077d95e95e6365af848177e647749a14c Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 01:12:03 +0800 Subject: [PATCH 06/18] Fix: Hunt zone view re-enabled by game - Fix: Validate switch status --- module/handler/strategy.py | 25 +++++++++++++++---------- module/map/fleet.py | 2 ++ module/ui/switch.py | 21 ++++++++++++++++++++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/module/handler/strategy.py b/module/handler/strategy.py index 7d17263d6..858dc3c4e 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -102,29 +102,32 @@ class StrategyHandler(InfoHandler): self.device.screenshot() - def strategy_set_execute(self, formation_index=2, sub_view=False, sub_hunt=False): + def strategy_set_execute(self, formation_index=None, sub_view=None, sub_hunt=None): """ Args: - formation_index (int): + formation_index (int): 1-3, or None for don't change sub_view (bool): sub_hunt (bool): + + Pages: + in: STRATEGY_OPENED """ logger.info(f'Strategy set: formation={formation_index}, submarine_view={sub_view}, submarine_hunt={sub_hunt}') - self.strategy_open() - formation.set(str(formation_index), main=self) + if formation_index is not None: + formation.set(str(formation_index), main=self) # Disable this until the icon bug of submarine zone is fixed # And don't enable MAP_HAS_DYNAMIC_RED_BORDER when using submarine # Submarine view check is back again, see SwitchWithHandler. # Don't know when but the game bug was fixed, remove the use of SwitchWithHandler - if submarine_view.appear(main=self): - submarine_view.set('on' if sub_view else 'off', main=self) - if submarine_hunt.appear(main=self): - submarine_hunt.set('on' if sub_hunt else 'off', main=self) - - self.strategy_close() + if sub_view is not None: + if submarine_view.appear(main=self): + submarine_view.set('on' if sub_view else 'off', main=self) + if sub_hunt is not None: + if submarine_hunt.appear(main=self): + submarine_hunt.set('on' if sub_hunt else 'off', main=self) def handle_strategy(self, index): """ @@ -143,11 +146,13 @@ class StrategyHandler(InfoHandler): self.__setattr__(f'fleet_{index}_formation_fixed', True) return False + self.strategy_open() self.strategy_set_execute( formation_index=expected_formation, sub_view=False, sub_hunt=bool(self.config.Submarine_Fleet) and self.config.Submarine_Mode == 'hunt_only' ) + self.strategy_close() self.__setattr__(f'fleet_{index}_formation_fixed', True) return True diff --git a/module/map/fleet.py b/module/map/fleet.py index e4f3ed782..995233a32 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -1061,6 +1061,8 @@ class Fleet(Camera, AmbushHandler): self.strategy_submarine_move_confirm() else: self.strategy_submarine_move_cancel() + # Hunt zone view re-enabled by game, after entering sub move mode + self.strategy_set_execute(sub_view=False) self.strategy_close() def submarine_move_near_boss(self, boss): diff --git a/module/ui/switch.py b/module/ui/switch.py index 015cb9824..e92531b30 100644 --- a/module/ui/switch.py +++ b/module/ui/switch.py @@ -1,6 +1,7 @@ from module.base.base import ModuleBase from module.base.button import Button from module.base.timer import Timer +from module.exception import ScriptError from module.logger import logger @@ -65,6 +66,21 @@ class Switch: return 'unknown' + def check_status(self, status): + """ + Args: + status (str): + + Returns: + bool: If status valid + """ + for row in self.status_list: + if row['status'] == status: + return True + + logger.warning(f'Switch {self.name} received an invalid status {status}') + raise ScriptError(f'Switch {self.name} received an invalid status {status}') + def set(self, status, main, skip_first_screenshot=True): """ Args: @@ -75,6 +91,8 @@ class Switch: Returns: bool: """ + self.check_status(status) + counter = 0 changed = False warning_show_timer = Timer(5, count=10).start() @@ -94,7 +112,8 @@ class Switch: logger.warning(f'Unknown {self.name} switch') warning_show_timer.reset() if counter >= 1: - logger.warning(f'{self.name} switch {status} asset has evaluated to unknown too many times, asset should be re-verified') + logger.warning(f'{self.name} switch {status} asset has evaluated to unknown too many times, ' + f'asset should be re-verified') return False counter += 1 continue From caf476a41dedc4d23ada89bf4dd5e7a8515befd9 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 17:49:07 +0800 Subject: [PATCH 07/18] Fix: Find missing submarines in find_submarine() --- module/map/fleet.py | 2221 ++++++++++++++++++++++--------------------- 1 file changed, 1120 insertions(+), 1101 deletions(-) diff --git a/module/map/fleet.py b/module/map/fleet.py index 995233a32..ba154577f 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -1,1101 +1,1120 @@ -import itertools - -import numpy as np - -from module.base.timer import Timer -from module.exception import MapWalkError, MapEnemyMoved, MapDetectionError -from module.handler.ambush import AmbushHandler -from module.logger import logger -from module.map.camera import Camera -from module.map.map_base import SelectedGrids -from module.map.map_base import location2node, location_ensure -from module.map.utils import match_movable - - -class Fleet(Camera, AmbushHandler): - fleet_1_location = () - fleet_2_location = () - fleet_submarine_location = () - battle_count = 0 - mystery_count = 0 - siren_count = 0 - fleet_ammo = 5 - ammo_count = 3 - - @property - def fleet_1(self): - if self.fleet_current_index != 1: - self.fleet_switch_to(index=1) - return self - - @fleet_1.setter - def fleet_1(self, value): - self.fleet_1_location = value - - @property - def fleet_2(self): - if self.config.FLEET_2 and self.config.FLEET_BOSS == 2: - if self.fleet_current_index != 2: - self.fleet_switch_to(index=2) - return self - - @fleet_2.setter - def fleet_2(self, value): - self.fleet_2_location = value - - @property - def fleet_submarine(self): - return self - - @fleet_submarine.setter - def fleet_submarine(self, value): - self.fleet_submarine_location = value - - @property - def fleet_current(self): - if self.fleet_current_index == 2: - return self.fleet_2_location - else: - return self.fleet_1_location - - @fleet_current.setter - def fleet_current(self, value): - if self.fleet_current_index == 2: - self.fleet_2_location = value - else: - self.fleet_1_location = value - - @property - def fleet_boss(self): - if self.config.FLEET_BOSS == 2 and self.config.FLEET_2: - return self.fleet_2 - else: - return self.fleet_1 - - @property - def fleet_boss_index(self): - if self.config.FLEET_BOSS == 2 and self.config.FLEET_2: - return 2 - else: - return 1 - - @property - def fleet_step(self): - if not self.config.MAP_HAS_FLEET_STEP: - return 0 - if self.fleet_current_index == 2: - return self.config.Fleet_Fleet2Step - else: - return self.config.Fleet_Fleet1Step - - def fleet_switch_to(self, index): - self.fleet_set(index=index) - self.camera = self.fleet_current - self.update() - self.find_path_initial() - self.map.show_cost() - self.show_fleet() - self.hp_get() - self.lv_get() - self.handle_strategy(index=self.fleet_current_index) - - def switch_to(self): - pass - - round = 0 - enemy_round = {} - - def round_next(self): - """ - Call this method after fleet arrived. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY and not self.config.MAP_HAS_MAZE: - return False - self.round += 1 - logger.info(f'Round: {self.round}, enemy_round: {self.enemy_round}') - - def round_battle(self, after_battle=True): - """ - Call this method after cleared an enemy. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - if not self.map.select(is_siren=True): - if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: - if not self.map.select(is_enemy=True): - self.enemy_round = {} - else: - self.enemy_round = {} - try: - data = self.map.spawn_data[self.battle_count] - except IndexError: - data = {} - enemy = data.get('siren', 0) - if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: - enemy += data.get('enemy', 0) - if enemy > 0: - r = self.round - self.enemy_round[r] = self.enemy_round.get(r, 0) + enemy - - def round_reset(self): - """ - Call this method after entering map. - """ - self.round = 0 - self.enemy_round = {} - - @property - def round_enemy_turn(self): - """ - Returns: - tuple[int]: Enemy moves once after player move X times. - It's a tuple because different enemy may have different X. - """ - if self.config.MAP_HAS_MOVABLE_ENEMY: - if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: - return tuple(set((list(self.config.MOVABLE_ENEMY_TURN) + list(self.config.MOVABLE_NORMAL_ENEMY_TURN)))) - else: - return self.config.MOVABLE_ENEMY_TURN - else: - if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: - return self.config.MOVABLE_NORMAL_ENEMY_TURN - else: - return tuple() - - @property - def round_is_new(self): - """ - Usually, MOVABLE_ENEMY_TURN = 2. - So a walk round is `player - player - enemy`, player moves twice, enemy moves once. - - Different sirens have different MOVABLE_ENEMY_TURN: - 2: Non-siren elite, SIREN_CL - 3: SIREN_CA - - Returns: - bool: If it's a new walk round, which means enemies have moved. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - for enemy in self.enemy_round.keys(): - for turn in self.round_enemy_turn: - if self.round - enemy > 0 and (self.round - enemy) % turn == 0: - return True - - return False - - @property - def round_wait(self): - """ - Returns: - float: Seconds to wait enemies moving. - """ - second = 0 - if self.config.MAP_HAS_MOVABLE_ENEMY: - count = 0 - for enemy, c in self.enemy_round.items(): - for turn in self.round_enemy_turn: - if self.round + 1 - enemy > 0 and (self.round + 1 - enemy) % turn == 0: - count += c - break - second += count * self.config.MAP_SIREN_MOVE_WAIT - - if self.config.MAP_HAS_MAZE: - if (self.round + 1) % 3 == 0: - second += 1.0 - - return second - - @property - def round_maze_changed(self): - """ - Returns: - bool: If maze changed at the start of this round. - """ - if not self.config.MAP_HAS_MAZE: - return False - return self.round != 0 and self.round % 3 == 0 - - def maze_active_on(self, grid): - """ - Args: - grid: - - Returns: - bool: If maze wall is on a the specific grid. - """ - if not self.config.MAP_HAS_MAZE: - return False - - grid = self.map[location_ensure(grid)] - if not grid.is_maze: - return False - return self.round % self.map.maze_round in grid.maze_round - - movable_before: SelectedGrids - movable_before_normal: SelectedGrids - - @property - def _walk_sight(self): - sight = self.map.camera_sight - return (sight[0], 0, sight[2], sight[3]) - - def _goto(self, location, expected=''): - """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. - - Args: - location (tuple, str, GridInfo): Destination. - expected (str): Expected result on destination grid, such as 'combat', 'combat_siren', 'mystery'. - Will give a waring if arrive with unexpected result. - """ - location = location_ensure(location) - result_mystery = '' - self.movable_before = self.map.select(is_siren=True) - self.movable_before_normal = self.map.select(is_enemy=True) - if self.hp_retreat_triggered(): - self.withdraw() - is_portal = self.map[location].is_portal - - while 1: - self.in_sight(location, sight=self._walk_sight) - self.focus_to_grid_center() - grid = self.convert_global_to_local(location) - - self.ambush_color_initial() - self.enemy_searching_color_initial() - grid.__str__ = location - result = 'nothing' - - self.device.click(grid) - arrived = False - # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. - extra = 0 - if self.config.Submarine_Mode == 'hunt_only': - extra += 4.5 - if self.config.MAP_HAS_LAND_BASED and grid.is_mechanism_trigger: - extra += grid.mechanism_wait - arrive_timer = Timer(0.5 + self.round_wait + extra, count=2) - arrive_unexpected_timer = Timer(1.5 + self.round_wait + extra, count=6) - # Wait after ambushed. - ambushed_retry = Timer(0.5) - # If nothing happens, click again. - walk_timeout = Timer(20) - walk_timeout.start() - - while 1: - self.device.screenshot() - self.view.update(image=self.device.image) - if is_portal: - self.update() - grid = self.view[self.view.center_loca] - - # Combat - if self.config.Campaign_UseFleetLock and not self.is_in_map(): - if self.handle_retirement(): - self.map_offensive() - walk_timeout.reset() - if self.handle_combat_low_emotion(): - walk_timeout.reset() - if self.combat_appear(): - self.combat( - expected_end=self._expected_end(expected), - fleet_index=self.fleet_show_index, - submarine_mode=self._submarine_mode(expected) - ) - self.hp_get() - self.lv_get(after_battle=True) - arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False - result = 'combat' - self.battle_count += 1 - self.fleet_ammo -= 1 - if 'siren' in expected or (self.config.MAP_HAS_MOVABLE_ENEMY and not expected): - self.siren_count += 1 - elif self.map[location].may_enemy: - self.map[location].is_cleared = True - - if self.catch_camera_repositioning(self.map[location]): - self.handle_boss_appear_refocus() - if self.config.MAP_FOCUS_ENEMY_AFTER_BATTLE: - self.camera = location - self.update() - grid = self.convert_global_to_local(location) - arrive_timer = Timer(0.5 + extra, count=2) - arrive_unexpected_timer = Timer(1.5 + extra, count=6) - walk_timeout.reset() - - # Ambush - if self.handle_ambush(): - self.hp_get() - self.lv_get(after_battle=True) - walk_timeout.reset() - self.view.update(image=self.device.image) - if not (grid.predict_fleet() and grid.predict_current_fleet()): - ambushed_retry.start() - - # Mystery - mystery = self.handle_mystery(button=grid) - if mystery: - self.mystery_count += 1 - result = 'mystery' - result_mystery = mystery - - # Cat attack animation - if self.handle_map_cat_attack(): - walk_timeout.reset() - continue - - # Guild popup - # Usually handled in combat_status, but sometimes delayed until after battle on slow PCs. - if self.handle_guild_popup_cancel(): - walk_timeout.reset() - continue - - if self.handle_walk_out_of_step(): - raise MapWalkError('walk_out_of_step') - - # Arrive - if self.is_in_map() and ( - grid.predict_fleet() - or (self.config.MAP_WALK_USE_CURRENT_FLEET and grid.predict_current_fleet()) - or (walk_timeout.reached() and grid.predict_current_fleet()) - ): - if not arrive_timer.started(): - logger.info(f'Arrive {location2node(location)}') - arrive_timer.start() - arrive_unexpected_timer.start() - if not arrive_timer.reached(): - continue - if expected and result not in expected: - if arrive_unexpected_timer.reached(): - logger.warning('Arrive with unexpected result') - else: - continue - if is_portal: - location = self.map[location].portal_link - self.camera = location - logger.info(f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}') - arrived = True - break - - # Story - if expected == 'story': - if self.handle_story_skip(): - result = 'story' - continue - - # End - if ambushed_retry.started() and ambushed_retry.reached(): - break - if walk_timeout.reached(): - logger.warning('Walk timeout. Retrying.') - self.predict() - self.ensure_edge_insight(skip_first_update=False) - break - - # End - if arrived: - # Ammo grid needs to click again, otherwise the next click doesn't work. - if self.map[location].may_ammo: - self.device.click(grid) - break - - self.map[self.fleet_current].is_fleet = False - self.map[location].wipe_out() - self.map[location].is_fleet = True - self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) - if result_mystery == 'get_carrier': - self.full_scan_carrier() - if result == 'combat': - self.round_battle(after_battle=True) - self.predict() - self.round_next() - if self.round_is_new: - if result != 'combat': - self.predict() - self.full_scan_movable(enemy_cleared=result == 'combat') - self.find_path_initial() - raise MapEnemyMoved - if self.round_maze_changed: - self.find_path_initial() - raise MapEnemyMoved - self.find_path_initial() - - def goto(self, location, optimize=None, expected=''): - """ - Args: - location (tuple, str, GridInfo): Destination. - optimize (bool): Optimize walk path, reducing ambushes. - If None, loads MAP_WALK_OPTIMIZE - expected (str): Expected result on destination grid, such as 'combat', 'combat_siren', 'mystery'. - Will give a waring if arrive with unexpected result. - """ - location = location_ensure(location) - if optimize is None: - optimize = self.config.MAP_WALK_OPTIMIZE - - # self.device.sleep(1000) - if optimize and (self.config.MAP_HAS_AMBUSH or self.config.MAP_HAS_FLEET_STEP or self.config.MAP_HAS_PORTAL - or self.config.MAP_HAS_MAZE): - nodes = self.map.find_path(location, step=self.fleet_step) - for node in nodes: - if self.maze_active_on(node): - logger.info(f'Maze is active on {location2node(node)}, bouncing to wait') - for _ in range(10): - grids = self.map[node].maze_nearby.delete(self.map.select(is_fleet=True)) - if grids.select(is_enemy=False): - grids = grids.select(is_enemy=False) - grids = grids.sort('cost') - self._goto(grids[0], expected='') - try: - self._goto(node, expected=expected if node == nodes[-1] else '') - except MapWalkError: - logger.warning('Map walk error.') - self.predict() - self.ensure_edge_insight() - nodes_ = self.map.find_path(node, step=1) - for node_ in nodes_: - self._goto(node_, expected=expected if node == nodes[-1] else '') - else: - self._goto(location, expected=expected) - - def find_path_initial(self): - """ - Call this method after fleet moved or entered map. - """ - if self.fleet_1_location: - self.map[self.fleet_1_location].is_fleet = True - if self.fleet_2_location: - self.map[self.fleet_2_location].is_fleet = True - location_dict = {} - if self.config.FLEET_2: - location_dict[2] = self.fleet_2_location - location_dict[1] = self.fleet_1_location - # Release fortress block - if self.config.MAP_HAS_FORTRESS: - if not self.map.select(is_fortress=True): - self.map.select(is_mechanism_block=True).set(is_mechanism_block=False) - self.map.find_path_initial_multi_fleet( - location_dict, current=self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) - - def show_fleet(self): - fleets = [] - for n in [1, 2]: - fleet = self.__getattribute__('fleet_%s_location' % n) - if len(fleet): - text = 'Fleet_%s: %s' % (n, location2node(fleet)) - if self.fleet_current_index == n: - text = '[%s]' % text - fleets.append(text) - logger.info(' '.join(fleets)) - - def show_submarine(self): - logger.info(f'Submarine: {location2node(self.fleet_submarine_location)}') - - def full_scan(self, queue=None, must_scan=None, mode='normal'): - super().full_scan( - queue=queue, must_scan=must_scan, battle_count=self.battle_count, mystery_count=self.mystery_count, - siren_count=self.siren_count, carrier_count=self.carrier_count, mode=mode) - - if self.config.FLEET_2 and not self.fleet_2_location: - fleets = self.map.select(is_fleet=True, is_current_fleet=False) - if fleets.count: - logger.info(f'Predict fleet_2 to be {fleets[0]}') - self.fleet_2_location = fleets[0].location - - for loca in [self.fleet_1_location, self.fleet_2_location]: - if len(loca) and loca in self.map: - grid = self.map[loca] - if grid.may_boss and grid.is_caught_by_siren: - # Only boss appears on fleet's face - pass - else: - self.map[loca].wipe_out() - - def full_scan_carrier(self): - """ - Call this method if get enemy searching in mystery. - """ - prev = self.map.select(is_enemy=True) - self.full_scan(mode='carrier') - diff = self.map.select(is_enemy=True).delete(prev) - logger.info(f'Carrier spawn: {diff}') - - def full_scan_movable(self, enemy_cleared=True): - """ - Call this method if enemy moved. - - Args: - enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. - False if just a simple walk and only need to scan movable enemies. - """ - if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: - if self.config.MAP_HAS_MOVABLE_ENEMY: - for grid in self.movable_before: - grid.wipe_out() - for grid in self.movable_before_normal: - grid.wipe_out() - self.full_scan(mode='movable') - self.track_movable(enemy_cleared=enemy_cleared, siren=True) - self.track_movable(enemy_cleared=enemy_cleared, siren=False) - else: - for grid in self.movable_before_normal: - grid.wipe_out() - self.full_scan(mode='movable') - self.track_movable(enemy_cleared=enemy_cleared, siren=False) - - elif self.config.MAP_HAS_MOVABLE_ENEMY: - for grid in self.movable_before: - grid.wipe_out() - self.full_scan(queue=None if enemy_cleared else self.movable_before, - must_scan=self.movable_before, mode='movable') - self.track_movable(enemy_cleared=enemy_cleared, siren=True) - - def track_movable(self, enemy_cleared=True, siren=True): - """ - Track enemy moving and predict missing enemies. - - Args: - enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. - False if just a simple walk and only need to scan movable enemies. - siren (bool): True if track sirens, false if track normal enemies - """ - # Track siren moving - before = self.movable_before if siren else self.movable_before_normal - after = self.map.select(is_siren=True) if siren else self.map.select(is_enemy=True) - step = self.config.MOVABLE_ENEMY_FLEET_STEP if siren else 1 - spawn = self.map.select(may_siren=True) if siren else self.map.select(may_enemy=True) - matched_before, matched_after = match_movable( - before=before.location, - spawn=spawn.location, - after=after.location, - fleets=[self.fleet_current] if enemy_cleared else [], - fleet_step=step - ) - matched_before = self.map.to_selected(matched_before) - matched_after = self.map.to_selected(matched_after) - logger.info(f'Movable enemy {before} -> {after}') - logger.info(f'Tracked enemy {matched_before} -> {matched_after}') - - # Delete wrong prediction - for grid in after.delete(matched_after): - if not grid.may_siren: - logger.warning(f'Wrong detection: {grid}') - grid.wipe_out() - - # Predict missing siren - diff = before.delete(matched_before) - _, missing = self.map.missing_get( - self.battle_count, self.mystery_count, self.siren_count, self.carrier_count, mode='normal') - missing = missing['siren'] if siren else missing['enemy'] - if diff and missing != 0: - logger.warning(f'Movable enemy tracking lost: {diff}') - covered = self.map.grid_covered(self.map[self.fleet_current], location=[(0, -2)]) - if self.fleet_1_location: - covered = covered.add(self.map.grid_covered(self.map[self.fleet_1_location], location=[(0, -1)])) - if self.fleet_2_location: - covered = covered.add(self.map.grid_covered(self.map[self.fleet_2_location], location=[(0, -1)])) - if siren: - for grid in after: - covered = covered.add(self.map.grid_covered(grid)) - else: - for grid in self.map.select(is_siren=True): - covered = covered.add(self.map.grid_covered(grid)) - logger.attr('enemy_covered', covered) - accessible = SelectedGrids([]) - for grid in diff: - self.map.find_path_initial(grid, has_ambush=False) - accessible = accessible.add(self.map.select(cost=0)).add(self.map.select(cost=1)) - if siren: - accessible = accessible.add(self.map.select(cost=2)) - self.map.find_path_initial(self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) - logger.attr('enemy_accessible', accessible) - predict = accessible.intersect(covered).select(is_sea=True, is_fleet=False) - logger.info(f'Movable enemy predict: {predict}') - matched_after = matched_after.add(predict) - for grid in predict: - if siren: - grid.is_siren = True - grid.is_enemy = True - elif missing == 0: - logger.info(f'Movable enemy tracking drop: {diff}') - - for grid in matched_after: - if grid.location != self.fleet_current: - grid.is_movable = True - - def find_all_fleets(self): - logger.hr('Find all fleets') - queue = self.map.select(is_spawn_point=True) - while queue: - queue = queue.sort_by_camera_distance(self.camera) - self.in_sight(queue[0], sight=(-1, 0, 1, 2)) - grid = self.convert_global_to_local(queue[0]) - if grid.predict_fleet(): - if grid.predict_current_fleet(): - self.fleet_1 = queue[0].location - else: - self.fleet_2 = queue[0].location - queue = queue[1:] - - def find_current_fleet(self): - logger.hr('Find current fleet') - if not self.config.POOR_MAP_DATA: - fleets = self.map.select(is_fleet=True, is_spawn_point=True) - else: - fleets = self.map.select(is_fleet=True) - logger.info('Fleets: %s' % str(fleets)) - count = fleets.count - if count == 1: - if not self.config.FLEET_2: - self.fleet_1 = fleets[0].location - else: - logger.info('Fleet_2 not detected.') - if self.config.POOR_MAP_DATA and not self.map.select(is_spawn_point=True): - self.fleet_1 = fleets[0].location - elif self.map.select(is_spawn_point=True).count == 2: - logger.info('Predict fleet to be spawn point') - another = self.map.select(is_spawn_point=True).delete(SelectedGrids([fleets[0]]))[0] - if fleets[0].is_current_fleet: - self.fleet_1 = fleets[0].location - self.fleet_2 = another.location - else: - self.fleet_1 = another.location - self.fleet_2 = fleets[0].location - else: - cover = self.map.grid_covered(fleets[0], location=[(0, -1)]) - if fleets[0].is_current_fleet and len(cover) and cover[0].is_spawn_point: - self.fleet_1 = fleets[0].location - self.fleet_2 = cover[0].location - else: - self.find_all_fleets() - elif count == 2: - current = self.map.select(is_current_fleet=True) - if current.count == 1: - self.fleet_1 = current[0].location - self.fleet_2 = fleets.delete(current)[0].location - else: - fleets = fleets.sort_by_camera_distance(self.camera) - self.in_sight(fleets[0], sight=(-1, 0, 1, 2)) - if self.convert_global_to_local(fleets[0]).predict_current_fleet(): - self.fleet_1 = fleets[0].location - self.fleet_2 = fleets[1].location - else: - self.in_sight(fleets[1], sight=(-1, 0, 1, 2)) - if self.convert_global_to_local(fleets[1]).predict_current_fleet(): - self.fleet_1 = fleets[1].location - self.fleet_2 = fleets[0].location - else: - logger.warning('Current fleet not found') - self.fleet_1 = fleets[0].location - self.fleet_2 = fleets[1].location - else: - if count == 0: - logger.warning('No fleets detected.') - fleets = self.map.select(is_current_fleet=True) - if fleets.count: - self.fleet_1 = fleets[0].location - if count > 2: - logger.warning('Too many fleets: %s.' % str(fleets)) - self.find_all_fleets() - - self.show_fleet() - return self.fleet_current - - def find_all_submarines(self): - logger.hr('Find all submarines') - queue = self.map.select(is_submarine_spawn_point=True) - while queue: - queue = queue.sort_by_camera_distance(self.camera) - self.in_sight(queue[0], sight=(-2, -1, 2, -1)) - grid = self.convert_global_to_local(queue[0]) - if grid.predict_submarine(): - self.fleet_submarine = queue[0].location - break - queue = queue[1:] - - def find_submarine(self): - if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): - return False - - fleets = self.map.select(is_submarine=True) - count = fleets.count - if count == 1: - self.fleet_submarine = fleets[0].location - elif count == 0: - logger.warning('No submarine found') - self.find_all_submarines() - else: - logger.warning('Too many submarines: %s.' % str(fleets)) - self.find_all_submarines() - - if not len(self.fleet_submarine_location): - logger.warning('Unable to find submarine, assume it is at map center') - shape = self.map.shape - center = (shape[0] // 2, shape[1] // 2) - self.fleet_submarine = self.map.select(is_land=False).sort_by_camera_distance(center)[0] - - self.show_submarine() - return self.fleet_submarine_location - - def map_init(self, map_): - """ - This method should be called after entering a map and before doing any operations. - - Args: - map_ (CampaignMap): - """ - logger.hr('Map init') - self.map_data_init(map_) - self.map_control_init() - - def map_data_init(self, map_): - """ - Init map data according to settings and map status. - Just data processing, no screenshots and clicks. - - Args: - map_ (CampaignMap): - """ - self.fleet_1_location = () - self.fleet_2_location = () - self.fleet_submarine_location = () - self.fleet_current_index = 1 - self.battle_count = 0 - self.mystery_count = 0 - self.carrier_count = 0 - self.siren_count = 0 - self.ammo_count = 3 - self.map = map_ - self.map.reset() - self.handle_clear_mode_config_cover() - self.map.poor_map_data = self.config.POOR_MAP_DATA - self.map.load_map_data(use_loop=self.map_is_clear_mode) - self.map.load_spawn_data(use_loop=self.map_is_clear_mode) - self.map.grid_connection_initial( - wall=self.config.MAP_HAS_WALL, - portal=self.config.MAP_HAS_PORTAL, - ) - self.map.load_mechanism( - land_based=self.config.MAP_HAS_LAND_BASED, - maze=self.config.MAP_HAS_MAZE, - fortress=self.config.MAP_HAS_FORTRESS - ) - - def map_control_init(self): - """ - Preparation before operations. - Such as select strategy, calculate hp and level, init camera position, do first map scan. - """ - self.update() - if not self.handle_fleet_reverse(): - self.fleet_set(index=1) - self.handle_strategy(index=self.fleet_show_index) - self.hp_reset() - self.hp_get() - self.lv_reset() - self.lv_get() - self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data) - self.handle_info_bar() # The info_bar which shows "Changed to fleet 2", will block the ammo icon - self.full_scan(must_scan=self.map.camera_data_spawn_point) - self.find_current_fleet() - self.find_submarine() - self.find_path_initial() - self.map.show_cost() - self.round_reset() - self.round_battle(after_battle=False) - - def handle_clear_mode_config_cover(self): - if not self.map_is_clear_mode: - return False - - if self.config.POOR_MAP_DATA and self.map.is_map_data_poor: - self.config.POOR_MAP_DATA = False - self.map.fortress_data = [(), ()] - - return True - - def _expected_end(self, expected): - for data in self.map.spawn_data: - if data.get('battle') == self.battle_count and 'boss' in expected: - return 'in_stage' - if data.get('battle') == self.battle_count + 1: - if data.get('enemy', 0) + data.get('siren', 0) + data.get('boss', 0) > 0: - return 'with_searching' - else: - return 'no_searching' - - if 'boss' in expected: - return 'in_stage' - - return None - - def _submarine_mode(self, expected): - if self.is_call_submarine_at_boss: - if 'boss' in expected: - return 'every_combat' - else: - return 'do_not_use' - else: - return None - - def fleet_at(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int): 1, 2 - - Returns: - bool: If fleet is at grid. - """ - if fleet is None: - return self.fleet_current == grid.location - if fleet == 1: - return self.fleet_1_location == grid.location - else: - return self.fleet_2_location == grid.location - - def check_accessibility(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int, str): 1, 2, 'boss' - - Returns: - bool: If accessible. - """ - if fleet is None: - return grid.is_accessible - if isinstance(fleet, str) and fleet.isdigit(): - fleet = int(fleet) - if fleet == 'boss': - fleet = self.fleet_boss_index - - if fleet == self.fleet_current_index: - return grid.is_accessible - else: - backup = self.fleet_current_index - self.fleet_current_index = fleet - self.find_path_initial() - result = grid.is_accessible - - self.fleet_current_index = backup - self.find_path_initial() - return result - - def brute_find_roadblocks(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int): 1, 2. Default to current fleet. - - Returns: - SelectedGrids: - """ - if fleet is not None and fleet != self.fleet_current_index: - backup = self.fleet_current_index - self.fleet_current_index = fleet - self.find_path_initial() - else: - backup = None - - if grid.is_accessible: - if backup is not None: - self.fleet_current_index = backup - self.find_path_initial() - return SelectedGrids([]) - - enemies = self.map.select(is_enemy=True) - logger.info(f'Potential enemy roadblocks: {enemies}') - for repeat in range(1, enemies.count + 1): - for select in itertools.product(enemies, repeat=repeat): - for block in select: - block.is_enemy = False - self.find_path_initial() - for block in select: - block.is_enemy = True - - if grid.is_accessible: - select = SelectedGrids(list(select)) - logger.info(f'Enemy roadblock: {select}') - if backup is not None: - self.fleet_current_index = backup - self.find_path_initial() - return select - - logger.warning('Enemy roadblock try exhausted.') - - def catch_camera_repositioning(self, destination): - """ - Args: - Destination (GridInfo): Globe map grid. - """ - appear = False - for data in self.map.spawn_data: - if data.get('battle') == self.battle_count and data.get('boss', 0): - logger.info('Catch camera re-positioning after boss appear') - appear = True - - # if self.config.POOR_MAP_DATA: - # self.device.screenshot() - # grids = Grids(self.device.image, config=self.config) - # grids.predict() - # grids.show() - # for grid in grids: - # if grid.is_boss: - # logger.info('Catch camera re-positioning after boss appear') - # appear = True - # for g in self.map: - # g.wipe_out() - # break - - return appear - - def handle_boss_appear_refocus(self, preset=None): - """ - Refocus to previous camera position after boss appear. - - Args: - preset (tuple): (x, y). - """ - camera = self.camera - if preset is None: - preset = self.config.MAP_BOSS_APPEAR_REFOCUS_SWIPE - - if preset is not None and np.linalg.norm(preset) > 0: - try: - self.update() - except MapDetectionError: - logger.info(f'MapDetectionError occurs after boss appear, trying swipe preset {preset}') - # Swipe optimize here may not be accurate. - self.map_swipe(preset) - self.ensure_edge_insight() - else: - self.update() - self.ensure_edge_insight() - - logger.info('Refocus to previous camera position.') - self.focus_to(camera) - - def fleet_checked_reset(self): - self.map_fleet_checked = False - self.fleet_1_formation_fixed = False - self.fleet_2_formation_fixed = False - - def _submarine_goto(self, location): - """ - Move submarine to given location. - - Args: - location (tuple, str, GridInfo): Destination. - - Returns: - bool: If submarine moved. - - Pages: - in: SUBMARINE_MOVE_CONFIRM - out: SUBMARINE_MOVE_CONFIRM - """ - location = location_ensure(location) - moved = True - while 1: - self.in_sight(location, sight=self._walk_sight) - self.focus_to_grid_center() - grid = self.convert_global_to_local(location) - grid.__str__ = location - - self.device.click(grid) - arrived = False - # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. - arrive_timer = Timer(0.1, count=2) - # If nothing happens, click again. - walk_timeout = Timer(2, count=6).start() - - while 1: - self.device.screenshot() - self.view.update(image=self.device.image) - - # Arrive - arrive_checker = grid.predict_submarine_move() - if grid.predict_submarine() or (walk_timeout.reached() and grid.predict_fleet()): - arrive_checker = True - moved = False - if arrive_checker: - if not arrive_timer.started(): - logger.info(f'Arrive {location2node(location)}') - arrive_timer.start() - if not arrive_timer.reached(): - continue - logger.info(f'Submarine arrive {location2node(location)} confirm.') - if not moved: - logger.info(f'Submarine already at {location2node(location)}') - arrived = True - break - - # End - if walk_timeout.reached(): - logger.warning('Walk timeout. Retrying.') - self.predict() - self.ensure_edge_insight(skip_first_update=False) - break - - # End - if arrived: - break - - return moved - - def submarine_goto(self, location): - """ - Open strategy, move submarine to given location, close strategy. - - Args: - location (tuple, str, GridInfo): Destination. - - Pages: - in: IN_MAP - out: IN_MAP - """ - self.strategy_open() - self.strategy_submarine_move_enter() - if self._submarine_goto(location): - self.strategy_submarine_move_confirm() - else: - self.strategy_submarine_move_cancel() - # Hunt zone view re-enabled by game, after entering sub move mode - self.strategy_set_execute(sub_view=False) - self.strategy_close() - - def submarine_move_near_boss(self, boss): - if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): - return False - - boss = location_ensure(boss) - logger.info(f'Move submarine near {location2node(boss)}') - - self.map.find_path_initial(self.fleet_submarine_location, has_ambush=False, has_enemy=False) - self.map.show_cost() - - def get_location(distance=2): - grids = self.map.select(is_land=False).filter( - lambda grid: np.sum(np.abs(np.subtract(grid.location, boss))) <= distance) - if grids: - return grids.sort('cost')[0].location - elif distance > 0: - logger.info(f'Unable to find a grid near boss in distance {distance}, fallback to {distance - 1}') - return get_location(distance - 1) - else: - logger.warning(f'Unable to find a grid near boss in distance {distance}, return boss position') - return boss - - distance_dict = { - 'to_boss_position': 0, - '1_grid_to_boss': 1, - '2_grid_to_boss': 2 - } - logger.attr('Distance to boss', self.config.Submarine_DistanceToBoss) - near = get_location(distance_dict.get(self.config.Submarine_DistanceToBoss, 0)) - - self.find_path_initial() - - logger.info(f'Move submarine to {location2node(near)}') - self.submarine_goto(near) +import itertools + +import numpy as np + +from module.base.timer import Timer +from module.exception import MapWalkError, MapEnemyMoved, MapDetectionError +from module.handler.ambush import AmbushHandler +from module.logger import logger +from module.map.camera import Camera +from module.map.map_base import SelectedGrids +from module.map.map_base import location2node, location_ensure +from module.map.utils import match_movable + + +class Fleet(Camera, AmbushHandler): + fleet_1_location = () + fleet_2_location = () + fleet_submarine_location = () + battle_count = 0 + mystery_count = 0 + siren_count = 0 + fleet_ammo = 5 + ammo_count = 3 + + @property + def fleet_1(self): + if self.fleet_current_index != 1: + self.fleet_switch_to(index=1) + return self + + @fleet_1.setter + def fleet_1(self, value): + self.fleet_1_location = value + + @property + def fleet_2(self): + if self.config.FLEET_2 and self.config.FLEET_BOSS == 2: + if self.fleet_current_index != 2: + self.fleet_switch_to(index=2) + return self + + @fleet_2.setter + def fleet_2(self, value): + self.fleet_2_location = value + + @property + def fleet_submarine(self): + return self + + @fleet_submarine.setter + def fleet_submarine(self, value): + self.fleet_submarine_location = value + + @property + def fleet_current(self): + if self.fleet_current_index == 2: + return self.fleet_2_location + else: + return self.fleet_1_location + + @fleet_current.setter + def fleet_current(self, value): + if self.fleet_current_index == 2: + self.fleet_2_location = value + else: + self.fleet_1_location = value + + @property + def fleet_boss(self): + if self.config.FLEET_BOSS == 2 and self.config.FLEET_2: + return self.fleet_2 + else: + return self.fleet_1 + + @property + def fleet_boss_index(self): + if self.config.FLEET_BOSS == 2 and self.config.FLEET_2: + return 2 + else: + return 1 + + @property + def fleet_step(self): + if not self.config.MAP_HAS_FLEET_STEP: + return 0 + if self.fleet_current_index == 2: + return self.config.Fleet_Fleet2Step + else: + return self.config.Fleet_Fleet1Step + + def fleet_switch_to(self, index): + self.fleet_set(index=index) + self.camera = self.fleet_current + self.update() + self.find_path_initial() + self.map.show_cost() + self.show_fleet() + self.hp_get() + self.lv_get() + self.handle_strategy(index=self.fleet_current_index) + + def switch_to(self): + pass + + round = 0 + enemy_round = {} + + def round_next(self): + """ + Call this method after fleet arrived. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY and not self.config.MAP_HAS_MAZE: + return False + self.round += 1 + logger.info(f'Round: {self.round}, enemy_round: {self.enemy_round}') + + def round_battle(self, after_battle=True): + """ + Call this method after cleared an enemy. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + if not self.map.select(is_siren=True): + if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: + if not self.map.select(is_enemy=True): + self.enemy_round = {} + else: + self.enemy_round = {} + try: + data = self.map.spawn_data[self.battle_count] + except IndexError: + data = {} + enemy = data.get('siren', 0) + if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: + enemy += data.get('enemy', 0) + if enemy > 0: + r = self.round + self.enemy_round[r] = self.enemy_round.get(r, 0) + enemy + + def round_reset(self): + """ + Call this method after entering map. + """ + self.round = 0 + self.enemy_round = {} + + @property + def round_enemy_turn(self): + """ + Returns: + tuple[int]: Enemy moves once after player move X times. + It's a tuple because different enemy may have different X. + """ + if self.config.MAP_HAS_MOVABLE_ENEMY: + if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: + return tuple(set((list(self.config.MOVABLE_ENEMY_TURN) + list(self.config.MOVABLE_NORMAL_ENEMY_TURN)))) + else: + return self.config.MOVABLE_ENEMY_TURN + else: + if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: + return self.config.MOVABLE_NORMAL_ENEMY_TURN + else: + return tuple() + + @property + def round_is_new(self): + """ + Usually, MOVABLE_ENEMY_TURN = 2. + So a walk round is `player - player - enemy`, player moves twice, enemy moves once. + + Different sirens have different MOVABLE_ENEMY_TURN: + 2: Non-siren elite, SIREN_CL + 3: SIREN_CA + + Returns: + bool: If it's a new walk round, which means enemies have moved. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + for enemy in self.enemy_round.keys(): + for turn in self.round_enemy_turn: + if self.round - enemy > 0 and (self.round - enemy) % turn == 0: + return True + + return False + + @property + def round_wait(self): + """ + Returns: + float: Seconds to wait enemies moving. + """ + second = 0 + if self.config.MAP_HAS_MOVABLE_ENEMY: + count = 0 + for enemy, c in self.enemy_round.items(): + for turn in self.round_enemy_turn: + if self.round + 1 - enemy > 0 and (self.round + 1 - enemy) % turn == 0: + count += c + break + second += count * self.config.MAP_SIREN_MOVE_WAIT + + if self.config.MAP_HAS_MAZE: + if (self.round + 1) % 3 == 0: + second += 1.0 + + return second + + @property + def round_maze_changed(self): + """ + Returns: + bool: If maze changed at the start of this round. + """ + if not self.config.MAP_HAS_MAZE: + return False + return self.round != 0 and self.round % 3 == 0 + + def maze_active_on(self, grid): + """ + Args: + grid: + + Returns: + bool: If maze wall is on a the specific grid. + """ + if not self.config.MAP_HAS_MAZE: + return False + + grid = self.map[location_ensure(grid)] + if not grid.is_maze: + return False + return self.round % self.map.maze_round in grid.maze_round + + movable_before: SelectedGrids + movable_before_normal: SelectedGrids + + @property + def _walk_sight(self): + sight = self.map.camera_sight + return (sight[0], 0, sight[2], sight[3]) + + def _goto(self, location, expected=''): + """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. + + Args: + location (tuple, str, GridInfo): Destination. + expected (str): Expected result on destination grid, such as 'combat', 'combat_siren', 'mystery'. + Will give a waring if arrive with unexpected result. + """ + location = location_ensure(location) + result_mystery = '' + self.movable_before = self.map.select(is_siren=True) + self.movable_before_normal = self.map.select(is_enemy=True) + if self.hp_retreat_triggered(): + self.withdraw() + is_portal = self.map[location].is_portal + + while 1: + self.in_sight(location, sight=self._walk_sight) + self.focus_to_grid_center() + grid = self.convert_global_to_local(location) + + self.ambush_color_initial() + self.enemy_searching_color_initial() + grid.__str__ = location + result = 'nothing' + + self.device.click(grid) + arrived = False + # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. + extra = 0 + if self.config.Submarine_Mode == 'hunt_only': + extra += 4.5 + if self.config.MAP_HAS_LAND_BASED and grid.is_mechanism_trigger: + extra += grid.mechanism_wait + arrive_timer = Timer(0.5 + self.round_wait + extra, count=2) + arrive_unexpected_timer = Timer(1.5 + self.round_wait + extra, count=6) + # Wait after ambushed. + ambushed_retry = Timer(0.5) + # If nothing happens, click again. + walk_timeout = Timer(20) + walk_timeout.start() + + while 1: + self.device.screenshot() + self.view.update(image=self.device.image) + if is_portal: + self.update() + grid = self.view[self.view.center_loca] + + # Combat + if self.config.Campaign_UseFleetLock and not self.is_in_map(): + if self.handle_retirement(): + self.map_offensive() + walk_timeout.reset() + if self.handle_combat_low_emotion(): + walk_timeout.reset() + if self.combat_appear(): + self.combat( + expected_end=self._expected_end(expected), + fleet_index=self.fleet_show_index, + submarine_mode=self._submarine_mode(expected) + ) + self.hp_get() + self.lv_get(after_battle=True) + arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False + result = 'combat' + self.battle_count += 1 + self.fleet_ammo -= 1 + if 'siren' in expected or (self.config.MAP_HAS_MOVABLE_ENEMY and not expected): + self.siren_count += 1 + elif self.map[location].may_enemy: + self.map[location].is_cleared = True + + if self.catch_camera_repositioning(self.map[location]): + self.handle_boss_appear_refocus() + if self.config.MAP_FOCUS_ENEMY_AFTER_BATTLE: + self.camera = location + self.update() + grid = self.convert_global_to_local(location) + arrive_timer = Timer(0.5 + extra, count=2) + arrive_unexpected_timer = Timer(1.5 + extra, count=6) + walk_timeout.reset() + + # Ambush + if self.handle_ambush(): + self.hp_get() + self.lv_get(after_battle=True) + walk_timeout.reset() + self.view.update(image=self.device.image) + if not (grid.predict_fleet() and grid.predict_current_fleet()): + ambushed_retry.start() + + # Mystery + mystery = self.handle_mystery(button=grid) + if mystery: + self.mystery_count += 1 + result = 'mystery' + result_mystery = mystery + + # Cat attack animation + if self.handle_map_cat_attack(): + walk_timeout.reset() + continue + + # Guild popup + # Usually handled in combat_status, but sometimes delayed until after battle on slow PCs. + if self.handle_guild_popup_cancel(): + walk_timeout.reset() + continue + + if self.handle_walk_out_of_step(): + raise MapWalkError('walk_out_of_step') + + # Arrive + if self.is_in_map() and ( + grid.predict_fleet() + or (self.config.MAP_WALK_USE_CURRENT_FLEET and grid.predict_current_fleet()) + or (walk_timeout.reached() and grid.predict_current_fleet()) + ): + if not arrive_timer.started(): + logger.info(f'Arrive {location2node(location)}') + arrive_timer.start() + arrive_unexpected_timer.start() + if not arrive_timer.reached(): + continue + if expected and result not in expected: + if arrive_unexpected_timer.reached(): + logger.warning('Arrive with unexpected result') + else: + continue + if is_portal: + location = self.map[location].portal_link + self.camera = location + logger.info(f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}') + arrived = True + break + + # Story + if expected == 'story': + if self.handle_story_skip(): + result = 'story' + continue + + # End + if ambushed_retry.started() and ambushed_retry.reached(): + break + if walk_timeout.reached(): + logger.warning('Walk timeout. Retrying.') + self.predict() + self.ensure_edge_insight(skip_first_update=False) + break + + # End + if arrived: + # Ammo grid needs to click again, otherwise the next click doesn't work. + if self.map[location].may_ammo: + self.device.click(grid) + break + + self.map[self.fleet_current].is_fleet = False + self.map[location].wipe_out() + self.map[location].is_fleet = True + self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) + if result_mystery == 'get_carrier': + self.full_scan_carrier() + if result == 'combat': + self.round_battle(after_battle=True) + self.predict() + self.round_next() + if self.round_is_new: + if result != 'combat': + self.predict() + self.full_scan_movable(enemy_cleared=result == 'combat') + self.find_path_initial() + raise MapEnemyMoved + if self.round_maze_changed: + self.find_path_initial() + raise MapEnemyMoved + self.find_path_initial() + + def goto(self, location, optimize=None, expected=''): + """ + Args: + location (tuple, str, GridInfo): Destination. + optimize (bool): Optimize walk path, reducing ambushes. + If None, loads MAP_WALK_OPTIMIZE + expected (str): Expected result on destination grid, such as 'combat', 'combat_siren', 'mystery'. + Will give a waring if arrive with unexpected result. + """ + location = location_ensure(location) + if optimize is None: + optimize = self.config.MAP_WALK_OPTIMIZE + + # self.device.sleep(1000) + if optimize and (self.config.MAP_HAS_AMBUSH or self.config.MAP_HAS_FLEET_STEP or self.config.MAP_HAS_PORTAL + or self.config.MAP_HAS_MAZE): + nodes = self.map.find_path(location, step=self.fleet_step) + for node in nodes: + if self.maze_active_on(node): + logger.info(f'Maze is active on {location2node(node)}, bouncing to wait') + for _ in range(10): + grids = self.map[node].maze_nearby.delete(self.map.select(is_fleet=True)) + if grids.select(is_enemy=False): + grids = grids.select(is_enemy=False) + grids = grids.sort('cost') + self._goto(grids[0], expected='') + try: + self._goto(node, expected=expected if node == nodes[-1] else '') + except MapWalkError: + logger.warning('Map walk error.') + self.predict() + self.ensure_edge_insight() + nodes_ = self.map.find_path(node, step=1) + for node_ in nodes_: + self._goto(node_, expected=expected if node == nodes[-1] else '') + else: + self._goto(location, expected=expected) + + def find_path_initial(self): + """ + Call this method after fleet moved or entered map. + """ + if self.fleet_1_location: + self.map[self.fleet_1_location].is_fleet = True + if self.fleet_2_location: + self.map[self.fleet_2_location].is_fleet = True + location_dict = {} + if self.config.FLEET_2: + location_dict[2] = self.fleet_2_location + location_dict[1] = self.fleet_1_location + # Release fortress block + if self.config.MAP_HAS_FORTRESS: + if not self.map.select(is_fortress=True): + self.map.select(is_mechanism_block=True).set(is_mechanism_block=False) + self.map.find_path_initial_multi_fleet( + location_dict, current=self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) + + def show_fleet(self): + fleets = [] + for n in [1, 2]: + fleet = self.__getattribute__('fleet_%s_location' % n) + if len(fleet): + text = 'Fleet_%s: %s' % (n, location2node(fleet)) + if self.fleet_current_index == n: + text = '[%s]' % text + fleets.append(text) + logger.info(' '.join(fleets)) + + def show_submarine(self): + logger.info(f'Submarine: {location2node(self.fleet_submarine_location)}') + + def full_scan(self, queue=None, must_scan=None, mode='normal'): + super().full_scan( + queue=queue, must_scan=must_scan, battle_count=self.battle_count, mystery_count=self.mystery_count, + siren_count=self.siren_count, carrier_count=self.carrier_count, mode=mode) + + if self.config.FLEET_2 and not self.fleet_2_location: + fleets = self.map.select(is_fleet=True, is_current_fleet=False) + if fleets.count: + logger.info(f'Predict fleet_2 to be {fleets[0]}') + self.fleet_2_location = fleets[0].location + + for loca in [self.fleet_1_location, self.fleet_2_location]: + if len(loca) and loca in self.map: + grid = self.map[loca] + if grid.may_boss and grid.is_caught_by_siren: + # Only boss appears on fleet's face + pass + else: + self.map[loca].wipe_out() + + def full_scan_carrier(self): + """ + Call this method if get enemy searching in mystery. + """ + prev = self.map.select(is_enemy=True) + self.full_scan(mode='carrier') + diff = self.map.select(is_enemy=True).delete(prev) + logger.info(f'Carrier spawn: {diff}') + + def full_scan_movable(self, enemy_cleared=True): + """ + Call this method if enemy moved. + + Args: + enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. + False if just a simple walk and only need to scan movable enemies. + """ + if self.config.MAP_HAS_MOVABLE_NORMAL_ENEMY: + if self.config.MAP_HAS_MOVABLE_ENEMY: + for grid in self.movable_before: + grid.wipe_out() + for grid in self.movable_before_normal: + grid.wipe_out() + self.full_scan(mode='movable') + self.track_movable(enemy_cleared=enemy_cleared, siren=True) + self.track_movable(enemy_cleared=enemy_cleared, siren=False) + else: + for grid in self.movable_before_normal: + grid.wipe_out() + self.full_scan(mode='movable') + self.track_movable(enemy_cleared=enemy_cleared, siren=False) + + elif self.config.MAP_HAS_MOVABLE_ENEMY: + for grid in self.movable_before: + grid.wipe_out() + self.full_scan(queue=None if enemy_cleared else self.movable_before, + must_scan=self.movable_before, mode='movable') + self.track_movable(enemy_cleared=enemy_cleared, siren=True) + + def track_movable(self, enemy_cleared=True, siren=True): + """ + Track enemy moving and predict missing enemies. + + Args: + enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. + False if just a simple walk and only need to scan movable enemies. + siren (bool): True if track sirens, false if track normal enemies + """ + # Track siren moving + before = self.movable_before if siren else self.movable_before_normal + after = self.map.select(is_siren=True) if siren else self.map.select(is_enemy=True) + step = self.config.MOVABLE_ENEMY_FLEET_STEP if siren else 1 + spawn = self.map.select(may_siren=True) if siren else self.map.select(may_enemy=True) + matched_before, matched_after = match_movable( + before=before.location, + spawn=spawn.location, + after=after.location, + fleets=[self.fleet_current] if enemy_cleared else [], + fleet_step=step + ) + matched_before = self.map.to_selected(matched_before) + matched_after = self.map.to_selected(matched_after) + logger.info(f'Movable enemy {before} -> {after}') + logger.info(f'Tracked enemy {matched_before} -> {matched_after}') + + # Delete wrong prediction + for grid in after.delete(matched_after): + if not grid.may_siren: + logger.warning(f'Wrong detection: {grid}') + grid.wipe_out() + + # Predict missing siren + diff = before.delete(matched_before) + _, missing = self.map.missing_get( + self.battle_count, self.mystery_count, self.siren_count, self.carrier_count, mode='normal') + missing = missing['siren'] if siren else missing['enemy'] + if diff and missing != 0: + logger.warning(f'Movable enemy tracking lost: {diff}') + covered = self.map.grid_covered(self.map[self.fleet_current], location=[(0, -2)]) + if self.fleet_1_location: + covered = covered.add(self.map.grid_covered(self.map[self.fleet_1_location], location=[(0, -1)])) + if self.fleet_2_location: + covered = covered.add(self.map.grid_covered(self.map[self.fleet_2_location], location=[(0, -1)])) + if siren: + for grid in after: + covered = covered.add(self.map.grid_covered(grid)) + else: + for grid in self.map.select(is_siren=True): + covered = covered.add(self.map.grid_covered(grid)) + logger.attr('enemy_covered', covered) + accessible = SelectedGrids([]) + for grid in diff: + self.map.find_path_initial(grid, has_ambush=False) + accessible = accessible.add(self.map.select(cost=0)).add(self.map.select(cost=1)) + if siren: + accessible = accessible.add(self.map.select(cost=2)) + self.map.find_path_initial(self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) + logger.attr('enemy_accessible', accessible) + predict = accessible.intersect(covered).select(is_sea=True, is_fleet=False) + logger.info(f'Movable enemy predict: {predict}') + matched_after = matched_after.add(predict) + for grid in predict: + if siren: + grid.is_siren = True + grid.is_enemy = True + elif missing == 0: + logger.info(f'Movable enemy tracking drop: {diff}') + + for grid in matched_after: + if grid.location != self.fleet_current: + grid.is_movable = True + + def find_all_fleets(self): + logger.hr('Find all fleets') + queue = self.map.select(is_spawn_point=True) + while queue: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0], sight=(-1, 0, 1, 2)) + grid = self.convert_global_to_local(queue[0]) + if grid.predict_fleet(): + if grid.predict_current_fleet(): + self.fleet_1 = queue[0].location + else: + self.fleet_2 = queue[0].location + queue = queue[1:] + + def find_current_fleet(self): + logger.hr('Find current fleet') + if not self.config.POOR_MAP_DATA: + fleets = self.map.select(is_fleet=True, is_spawn_point=True) + else: + fleets = self.map.select(is_fleet=True) + logger.info('Fleets: %s' % str(fleets)) + count = fleets.count + if count == 1: + if not self.config.FLEET_2: + self.fleet_1 = fleets[0].location + else: + logger.info('Fleet_2 not detected.') + if self.config.POOR_MAP_DATA and not self.map.select(is_spawn_point=True): + self.fleet_1 = fleets[0].location + elif self.map.select(is_spawn_point=True).count == 2: + logger.info('Predict fleet to be spawn point') + another = self.map.select(is_spawn_point=True).delete(SelectedGrids([fleets[0]]))[0] + if fleets[0].is_current_fleet: + self.fleet_1 = fleets[0].location + self.fleet_2 = another.location + else: + self.fleet_1 = another.location + self.fleet_2 = fleets[0].location + else: + cover = self.map.grid_covered(fleets[0], location=[(0, -1)]) + if fleets[0].is_current_fleet and len(cover) and cover[0].is_spawn_point: + self.fleet_1 = fleets[0].location + self.fleet_2 = cover[0].location + else: + self.find_all_fleets() + elif count == 2: + current = self.map.select(is_current_fleet=True) + if current.count == 1: + self.fleet_1 = current[0].location + self.fleet_2 = fleets.delete(current)[0].location + else: + fleets = fleets.sort_by_camera_distance(self.camera) + self.in_sight(fleets[0], sight=(-1, 0, 1, 2)) + if self.convert_global_to_local(fleets[0]).predict_current_fleet(): + self.fleet_1 = fleets[0].location + self.fleet_2 = fleets[1].location + else: + self.in_sight(fleets[1], sight=(-1, 0, 1, 2)) + if self.convert_global_to_local(fleets[1]).predict_current_fleet(): + self.fleet_1 = fleets[1].location + self.fleet_2 = fleets[0].location + else: + logger.warning('Current fleet not found') + self.fleet_1 = fleets[0].location + self.fleet_2 = fleets[1].location + else: + if count == 0: + logger.warning('No fleets detected.') + fleets = self.map.select(is_current_fleet=True) + if fleets.count: + self.fleet_1 = fleets[0].location + if count > 2: + logger.warning('Too many fleets: %s.' % str(fleets)) + self.find_all_fleets() + + self.show_fleet() + return self.fleet_current + + def find_all_submarines(self): + logger.hr('Find all submarines') + queue = self.map.select(is_submarine_spawn_point=True) + while queue: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0], sight=(-2, -1, 2, -1)) + grid = self.convert_global_to_local(queue[0]) + if grid.predict_submarine(): + self.fleet_submarine = queue[0].location + break + queue = queue[1:] + + def find_submarine(self): + if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): + return False + + fleets = self.map.select(is_submarine=True) + count = fleets.count + if count == 1: + self.fleet_submarine = fleets[0].location + elif count == 0: + logger.info('No submarine found') + # Try spawn points + spawn_point = self.map.select(is_submarine_spawn_point=True) + if spawn_point.count == 1: + logger.info(f'Predict the only submarine spawn point {spawn_point[0]} as submarine') + self.fleet_submarine = spawn_point[0].location + else: + logger.info(f'Having multiple submarine spawn points: {spawn_point}') + # Try covered grids + covered = SelectedGrids([]) + for grid in spawn_point: + covered = covered.add(self.map.grid_covered(grid, location=[(0, 1)])) + covered = covered.filter(lambda g: g.is_enemy or g.is_fleet or g.is_siren or g.is_boss) + if covered.count == 1: + spawn_point = self.map.grid_covered(covered[0], location=[(0, -1)]) + logger.info(f'Submarine {spawn_point[0]} covered by {covered[0]}') + self.fleet_submarine = spawn_point[0].location + else: + logger.info('Found multiple submarine spawn points being covered') + # Give up + self.find_all_submarines() + else: + logger.warning('Too many submarines: %s.' % str(fleets)) + self.find_all_submarines() + + if not len(self.fleet_submarine_location): + logger.warning('Unable to find submarine, assume it is at map center') + shape = self.map.shape + center = (shape[0] // 2, shape[1] // 2) + self.fleet_submarine = self.map.select(is_land=False).sort_by_camera_distance(center)[0].location + + self.show_submarine() + return self.fleet_submarine_location + + def map_init(self, map_): + """ + This method should be called after entering a map and before doing any operations. + + Args: + map_ (CampaignMap): + """ + logger.hr('Map init') + self.map_data_init(map_) + self.map_control_init() + + def map_data_init(self, map_): + """ + Init map data according to settings and map status. + Just data processing, no screenshots and clicks. + + Args: + map_ (CampaignMap): + """ + self.fleet_1_location = () + self.fleet_2_location = () + self.fleet_submarine_location = () + self.fleet_current_index = 1 + self.battle_count = 0 + self.mystery_count = 0 + self.carrier_count = 0 + self.siren_count = 0 + self.ammo_count = 3 + self.map = map_ + self.map.reset() + self.handle_clear_mode_config_cover() + self.map.poor_map_data = self.config.POOR_MAP_DATA + self.map.load_map_data(use_loop=self.map_is_clear_mode) + self.map.load_spawn_data(use_loop=self.map_is_clear_mode) + self.map.grid_connection_initial( + wall=self.config.MAP_HAS_WALL, + portal=self.config.MAP_HAS_PORTAL, + ) + self.map.load_mechanism( + land_based=self.config.MAP_HAS_LAND_BASED, + maze=self.config.MAP_HAS_MAZE, + fortress=self.config.MAP_HAS_FORTRESS + ) + + def map_control_init(self): + """ + Preparation before operations. + Such as select strategy, calculate hp and level, init camera position, do first map scan. + """ + self.update() + if not self.handle_fleet_reverse(): + self.fleet_set(index=1) + self.handle_strategy(index=self.fleet_show_index) + self.hp_reset() + self.hp_get() + self.lv_reset() + self.lv_get() + self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data) + self.handle_info_bar() # The info_bar which shows "Changed to fleet 2", will block the ammo icon + self.full_scan(must_scan=self.map.camera_data_spawn_point) + self.find_current_fleet() + self.find_submarine() + self.find_path_initial() + self.map.show_cost() + self.round_reset() + self.round_battle(after_battle=False) + + def handle_clear_mode_config_cover(self): + if not self.map_is_clear_mode: + return False + + if self.config.POOR_MAP_DATA and self.map.is_map_data_poor: + self.config.POOR_MAP_DATA = False + self.map.fortress_data = [(), ()] + + return True + + def _expected_end(self, expected): + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and 'boss' in expected: + return 'in_stage' + if data.get('battle') == self.battle_count + 1: + if data.get('enemy', 0) + data.get('siren', 0) + data.get('boss', 0) > 0: + return 'with_searching' + else: + return 'no_searching' + + if 'boss' in expected: + return 'in_stage' + + return None + + def _submarine_mode(self, expected): + if self.is_call_submarine_at_boss: + if 'boss' in expected: + return 'every_combat' + else: + return 'do_not_use' + else: + return None + + def fleet_at(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2 + + Returns: + bool: If fleet is at grid. + """ + if fleet is None: + return self.fleet_current == grid.location + if fleet == 1: + return self.fleet_1_location == grid.location + else: + return self.fleet_2_location == grid.location + + def check_accessibility(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int, str): 1, 2, 'boss' + + Returns: + bool: If accessible. + """ + if fleet is None: + return grid.is_accessible + if isinstance(fleet, str) and fleet.isdigit(): + fleet = int(fleet) + if fleet == 'boss': + fleet = self.fleet_boss_index + + if fleet == self.fleet_current_index: + return grid.is_accessible + else: + backup = self.fleet_current_index + self.fleet_current_index = fleet + self.find_path_initial() + result = grid.is_accessible + + self.fleet_current_index = backup + self.find_path_initial() + return result + + def brute_find_roadblocks(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2. Default to current fleet. + + Returns: + SelectedGrids: + """ + if fleet is not None and fleet != self.fleet_current_index: + backup = self.fleet_current_index + self.fleet_current_index = fleet + self.find_path_initial() + else: + backup = None + + if grid.is_accessible: + if backup is not None: + self.fleet_current_index = backup + self.find_path_initial() + return SelectedGrids([]) + + enemies = self.map.select(is_enemy=True) + logger.info(f'Potential enemy roadblocks: {enemies}') + for repeat in range(1, enemies.count + 1): + for select in itertools.product(enemies, repeat=repeat): + for block in select: + block.is_enemy = False + self.find_path_initial() + for block in select: + block.is_enemy = True + + if grid.is_accessible: + select = SelectedGrids(list(select)) + logger.info(f'Enemy roadblock: {select}') + if backup is not None: + self.fleet_current_index = backup + self.find_path_initial() + return select + + logger.warning('Enemy roadblock try exhausted.') + + def catch_camera_repositioning(self, destination): + """ + Args: + Destination (GridInfo): Globe map grid. + """ + appear = False + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and data.get('boss', 0): + logger.info('Catch camera re-positioning after boss appear') + appear = True + + # if self.config.POOR_MAP_DATA: + # self.device.screenshot() + # grids = Grids(self.device.image, config=self.config) + # grids.predict() + # grids.show() + # for grid in grids: + # if grid.is_boss: + # logger.info('Catch camera re-positioning after boss appear') + # appear = True + # for g in self.map: + # g.wipe_out() + # break + + return appear + + def handle_boss_appear_refocus(self, preset=None): + """ + Refocus to previous camera position after boss appear. + + Args: + preset (tuple): (x, y). + """ + camera = self.camera + if preset is None: + preset = self.config.MAP_BOSS_APPEAR_REFOCUS_SWIPE + + if preset is not None and np.linalg.norm(preset) > 0: + try: + self.update() + except MapDetectionError: + logger.info(f'MapDetectionError occurs after boss appear, trying swipe preset {preset}') + # Swipe optimize here may not be accurate. + self.map_swipe(preset) + self.ensure_edge_insight() + else: + self.update() + self.ensure_edge_insight() + + logger.info('Refocus to previous camera position.') + self.focus_to(camera) + + def fleet_checked_reset(self): + self.map_fleet_checked = False + self.fleet_1_formation_fixed = False + self.fleet_2_formation_fixed = False + + def _submarine_goto(self, location): + """ + Move submarine to given location. + + Args: + location (tuple, str, GridInfo): Destination. + + Returns: + bool: If submarine moved. + + Pages: + in: SUBMARINE_MOVE_CONFIRM + out: SUBMARINE_MOVE_CONFIRM + """ + location = location_ensure(location) + moved = True + while 1: + self.in_sight(location, sight=self._walk_sight) + self.focus_to_grid_center() + grid = self.convert_global_to_local(location) + grid.__str__ = location + + self.device.click(grid) + arrived = False + # Usually no need to wait + arrive_timer = Timer(0.1, count=0) + # If nothing happens, click again. + walk_timeout = Timer(2, count=6).start() + + while 1: + self.device.screenshot() + self.view.update(image=self.device.image) + + # Arrive + arrive_checker = grid.predict_submarine_move() + if grid.predict_submarine() or (walk_timeout.reached() and grid.predict_fleet()): + arrive_checker = True + moved = False + if arrive_checker: + if not arrive_timer.started(): + logger.info(f'Arrive {location2node(location)}') + arrive_timer.start() + if not arrive_timer.reached(): + continue + logger.info(f'Submarine arrive {location2node(location)} confirm.') + if not moved: + logger.info(f'Submarine already at {location2node(location)}') + arrived = True + break + + # End + if walk_timeout.reached(): + logger.warning('Walk timeout. Retrying.') + self.predict() + self.ensure_edge_insight(skip_first_update=False) + break + + # End + if arrived: + break + + return moved + + def submarine_goto(self, location): + """ + Open strategy, move submarine to given location, close strategy. + + Args: + location (tuple, str, GridInfo): Destination. + + Pages: + in: IN_MAP + out: IN_MAP + """ + self.strategy_open() + self.strategy_submarine_move_enter() + if self._submarine_goto(location): + self.strategy_submarine_move_confirm() + else: + self.strategy_submarine_move_cancel() + # Hunt zone view re-enabled by game, after entering sub move mode + self.strategy_set_execute(sub_view=False) + self.strategy_close() + + def submarine_move_near_boss(self, boss): + if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): + return False + + boss = location_ensure(boss) + logger.info(f'Move submarine near {location2node(boss)}') + + self.map.find_path_initial(self.fleet_submarine_location, has_ambush=False, has_enemy=False) + self.map.show_cost() + + def get_location(distance=2): + grids = self.map.select(is_land=False).filter( + lambda grid: np.sum(np.abs(np.subtract(grid.location, boss))) <= distance) + if grids: + return grids.sort('cost')[0].location + elif distance > 0: + logger.info(f'Unable to find a grid near boss in distance {distance}, fallback to {distance - 1}') + return get_location(distance - 1) + else: + logger.warning(f'Unable to find a grid near boss in distance {distance}, return boss position') + return boss + + distance_dict = { + 'to_boss_position': 0, + '1_grid_to_boss': 1, + '2_grid_to_boss': 2 + } + logger.attr('Distance to boss', self.config.Submarine_DistanceToBoss) + near = get_location(distance_dict.get(self.config.Submarine_DistanceToBoss, 0)) + + self.find_path_initial() + + logger.info(f'Move submarine to {location2node(near)}') + self.submarine_goto(near) From 1abc415a0342b53f94fcefaa756bb72da4287a4a Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 18:46:36 +0800 Subject: [PATCH 08/18] Fix: Submarine on the upper grid is predicted as fleet arrived (#826) --- campaign/campaign_main/campaign_13_4.py | 2 +- module/map/fleet.py | 28 ++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/campaign/campaign_main/campaign_13_4.py b/campaign/campaign_main/campaign_13_4.py index 3230af3ca..20c3458ff 100644 --- a/campaign/campaign_main/campaign_13_4.py +++ b/campaign/campaign_main/campaign_13_4.py @@ -20,7 +20,7 @@ MAP.map_data = """ MAP.weight_data = """ 50 50 90 90 90 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 90 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 diff --git a/module/map/fleet.py b/module/map/fleet.py index ba154577f..8ccd677aa 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -255,6 +255,9 @@ class Fleet(Camera, AmbushHandler): if self.hp_retreat_triggered(): self.withdraw() is_portal = self.map[location].is_portal + # The upper grid is submarine, may mess up predict_fleet() + may_submarine_icon = self.map.grid_covered(self.map[location], location=[(0, -1)]) + may_submarine_icon = may_submarine_icon and self.fleet_submarine_location == may_submarine_icon[0].location while 1: self.in_sight(location, sight=self._walk_sight) @@ -354,13 +357,24 @@ class Fleet(Camera, AmbushHandler): raise MapWalkError('walk_out_of_step') # Arrive - if self.is_in_map() and ( - grid.predict_fleet() - or (self.config.MAP_WALK_USE_CURRENT_FLEET and grid.predict_current_fleet()) - or (walk_timeout.reached() and grid.predict_current_fleet()) - ): + arrive_predict = '' + arrive_checker = False + if self.is_in_map(): + if not may_submarine_icon and grid.predict_fleet(): + arrive_predict = '(is_fleet)' + arrive_checker = True + elif may_submarine_icon and grid.predict_current_fleet(): + arrive_predict = '(may_submarine_icon, is_current_fleet)' + arrive_checker = True + elif self.config.MAP_WALK_USE_CURRENT_FLEET and grid.predict_current_fleet(): + arrive_predict = '(MAP_WALK_USE_CURRENT_FLEET, is_current_fleet)' + arrive_checker = True + elif walk_timeout.reached() and grid.predict_current_fleet(): + arrive_predict = '(walk_timeout, is_current_fleet)' + arrive_checker = True + if arrive_checker: if not arrive_timer.started(): - logger.info(f'Arrive {location2node(location)}') + logger.info(f'Arrive {location2node(location)} {arrive_predict}'.strip()) arrive_timer.start() arrive_unexpected_timer.start() if not arrive_timer.reached(): @@ -714,7 +728,7 @@ class Fleet(Camera, AmbushHandler): queue = queue[1:] def find_submarine(self): - if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): + if not (self.config.SUBMARINE and self.map.select(is_submarine_spawn_point=True)): return False fleets = self.map.select(is_submarine=True) From 50b5b493650c331c852a1d65d04ed8b0fb0dbcbd Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 20:35:07 +0800 Subject: [PATCH 09/18] Opt: Faster Switch.set(), replace click-wait with click interval --- module/campaign/campaign_ui.py | 8 ++--- module/handler/strategy.py | 56 ---------------------------------- module/retire/dock.py | 8 ++--- module/ui/switch.py | 50 +++++++++++++++++++++--------- 4 files changed, 44 insertions(+), 78 deletions(-) diff --git a/module/campaign/campaign_ui.py b/module/campaign/campaign_ui.py index ebbdb0c39..6296e4a4c 100644 --- a/module/campaign/campaign_ui.py +++ b/module/campaign/campaign_ui.py @@ -7,11 +7,11 @@ from module.ui.ui import UI, page_campaign, page_event, page_sp STAGE_SHOWN_WAIT = (1, 1.2) MODE_SWITCH_1 = Switch('Mode_switch_1', offset=(30, 10)) -MODE_SWITCH_1.add_status('normal', SWITCH_1_NORMAL, sleep=STAGE_SHOWN_WAIT) -MODE_SWITCH_1.add_status('hard', SWITCH_1_HARD, sleep=STAGE_SHOWN_WAIT) +MODE_SWITCH_1.add_status('normal', SWITCH_1_NORMAL) +MODE_SWITCH_1.add_status('hard', SWITCH_1_HARD) MODE_SWITCH_2 = Switch('Mode_switch_2', offset=(30, 10)) -MODE_SWITCH_2.add_status('hard', SWITCH_2_HARD, sleep=STAGE_SHOWN_WAIT) -MODE_SWITCH_2.add_status('ex', SWITCH_2_EX, sleep=STAGE_SHOWN_WAIT) +MODE_SWITCH_2.add_status('hard', SWITCH_2_HARD) +MODE_SWITCH_2.add_status('ex', SWITCH_2_EX) class CampaignUI(UI, CampaignOcr): diff --git a/module/handler/strategy.py b/module/handler/strategy.py index 858dc3c4e..2d872bcdd 100644 --- a/module/handler/strategy.py +++ b/module/handler/strategy.py @@ -1,6 +1,5 @@ import numpy as np -from module.base.timer import Timer from module.handler.assets import * from module.handler.info_handler import InfoHandler from module.logger import logger @@ -16,61 +15,6 @@ submarine_hunt = Switch('Submarine_hunt', offset=120) submarine_hunt.add_status('on', check_button=SUBMARINE_HUNT_ON) submarine_hunt.add_status('off', check_button=SUBMARINE_HUNT_OFF) - -class SwitchWithHandler(Switch): - @staticmethod - def handle_submarine_zone_icon_bug(main): - """ - When switching the submarine zone, the icon in the strategy don't change. - If click submarine hunt, submarine zone will show the correct icon. - So the key to deal with submarine zone icon bug, is to double click submarine_hunt. - - Args: - main (ModuleBase): - """ - current = submarine_hunt.get(main=main) - opposite = 'off' if current == 'on' else 'on' - submarine_hunt.set(opposite, main=main) - submarine_hunt.set(current, main=main) - - def set(self, status, main, skip_first_screenshot=True): - """ - Args: - status (str): - main (ModuleBase): - skip_first_screenshot (bool): - - Returns: - bool: - """ - changed = False - warning_show_timer = Timer(5, count=10).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - main.device.screenshot() - print('scr') - - current = self.get(main=main) - logger.attr(self.name, current) - if current == status: - return changed - - if current == 'unknown': - if warning_show_timer.reached(): - logger.warning(f'Unknown {self.name} switch') - warning_show_timer.reset() - continue - - for data in self.status_list: - if data['status'] == current: - main.device.click(data['click_button']) - main.device.sleep(data['sleep']) - self.handle_submarine_zone_icon_bug(main=main) # Different from Switch. - changed = True - - submarine_view = Switch('Submarine_view', offset=120) submarine_view.add_status('on', check_button=SUBMARINE_VIEW_ON) submarine_view.add_status('off', check_button=SUBMARINE_VIEW_OFF) diff --git a/module/retire/dock.py b/module/retire/dock.py index d51f33d71..734e21270 100644 --- a/module/retire/dock.py +++ b/module/retire/dock.py @@ -8,12 +8,12 @@ from module.ui.scroll import Scroll from module.ui.switch import Switch DOCK_SORTING = Switch('Dork_sorting') -DOCK_SORTING.add_status('Ascending', check_button=SORT_ASC, click_button=SORTING_CLICK, sleep=(0.3, 0.5)) -DOCK_SORTING.add_status('Descending', check_button=SORT_DESC, click_button=SORTING_CLICK, sleep=(0.3, 0.5)) +DOCK_SORTING.add_status('Ascending', check_button=SORT_ASC, click_button=SORTING_CLICK) +DOCK_SORTING.add_status('Descending', check_button=SORT_DESC, click_button=SORTING_CLICK) DOCK_FAVOURITE = Switch('Favourite_filter') -DOCK_FAVOURITE.add_status('on', check_button=COMMON_SHIP_FILTER_ENABLE, sleep=(0.3, 0.5)) -DOCK_FAVOURITE.add_status('off', check_button=COMMON_SHIP_FILTER_DISABLE, sleep=(0.3, 0.5)) +DOCK_FAVOURITE.add_status('on', check_button=COMMON_SHIP_FILTER_ENABLE) +DOCK_FAVOURITE.add_status('off', check_button=COMMON_SHIP_FILTER_DISABLE) FILTER_SORT_GRIDS = ButtonGrid( origin=(284, 60), delta=(158, 0), button_shape=(137, 38), grid_shape=(6, 1), name='FILTER_SORT') diff --git a/module/ui/switch.py b/module/ui/switch.py index e92531b30..301bbc344 100644 --- a/module/ui/switch.py +++ b/module/ui/switch.py @@ -6,6 +6,20 @@ from module.logger import logger class Switch: + """ + A wrapper to handle switches in game. + Set switch status with reties. + + Examples: + # Definitions + submarine_hunt = Switch('Submarine_hunt', offset=120) + submarine_hunt.add_status('on', check_button=SUBMARINE_HUNT_ON) + submarine_hunt.add_status('off', check_button=SUBMARINE_HUNT_OFF) + + # Change status to ON + submarine_view.set('on', main=self) + """ + def __init__(self, name='Switch', is_selector=False, offset=0): """ Args: @@ -21,21 +35,19 @@ class Switch: self.offset = offset self.status_list = [] - def add_status(self, status, check_button, click_button=None, offset=0, sleep=(1.0, 1.2)): + def add_status(self, status, check_button, click_button=None, offset=0): """ Args: status (str): check_button (Button): click_button (Button): offset (bool, int, tuple): - sleep (int, float, tuple): """ self.status_list.append({ 'status': status, 'check_button': check_button, 'click_button': click_button if click_button is not None else check_button, - 'offset': offset if offset else self.offset, - 'sleep': sleep + 'offset': offset if offset else self.offset }) def appear(self, main): @@ -66,17 +78,20 @@ class Switch: return 'unknown' - def check_status(self, status): + def get_data(self, status): """ Args: status (str): Returns: - bool: If status valid + dict: Dictionary in add_status + + Raises: + ScriptError: If status invalid """ for row in self.status_list: if row['status'] == status: - return True + return row logger.warning(f'Switch {self.name} received an invalid status {status}') raise ScriptError(f'Switch {self.name} received an invalid status {status}') @@ -91,22 +106,27 @@ class Switch: Returns: bool: """ - self.check_status(status) + self.get_data(status) counter = 0 changed = False warning_show_timer = Timer(5, count=10).start() + click_timer = Timer(1, count=3) while 1: if skip_first_screenshot: skip_first_screenshot = False else: main.device.screenshot() + # Detect current = self.get(main=main) logger.attr(self.name, current) + + # End if current == status: return changed + # Warning if current == 'unknown': if warning_show_timer.reached(): logger.warning(f'Unknown {self.name} switch') @@ -118,9 +138,11 @@ class Switch: counter += 1 continue - click_status = status if self.is_choice else current - for data in self.status_list: - if data['status'] == click_status: - main.device.click(data['click_button']) - main.device.sleep(data['sleep']) - changed = True + # Click + if click_timer.reached(): + click_status = status if self.is_choice else current + main.device.click(self.get_data(click_status)['click_button']) + click_timer.reset() + changed = True + + return changed From 84917ab4e4d88c2a462431cd73c841c76205f8e3 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:16:58 +0800 Subject: [PATCH 10/18] Fix: 5 is not in list, if focused on a region 5 zone in find_siren_stronghold() - Lower hue to 285 to fix random detection issues --- module/os/globe_camera.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/module/os/globe_camera.py b/module/os/globe_camera.py index 5cb2fbcc1..6c1811fd7 100644 --- a/module/os/globe_camera.py +++ b/module/os/globe_camera.py @@ -179,7 +179,7 @@ class GlobeCamera(GlobeOperation, ZoneManager): center = np.array([[cv2.mean(center), ], ]).astype(np.uint8) h, s, v = rgb2hsv(center)[0][0] # hsv usually to be (338, 74.9, 100) - if 290 < h <= 360 and s > 45 and v > 45: + if 285 < h <= 360 and s > 45 and v > 45: return True else: return False @@ -233,8 +233,16 @@ class GlobeCamera(GlobeOperation, ZoneManager): """ logger.hr(f'Find siren stronghold', level=1) region = self.camera_to_zone(self.globe_camera).region - order = [1, 2, 4, 3] * 2 + order = [1, 2, 4, 3] + if region not in order: + # Camera may focus on region 5, select the nearest non-region-5 zone + zones = self.zones.delete(self.zones.select(region=5)) \ + .delete(self.zones.select(is_port=True)) \ + .sort_by_camera_distance(self.globe_camera) + region = zones[0].region + index = order.index(region) + order = order * 2 order = order[index:index + 4] for region in order: logger.hr(f'Find siren stronghold in region {region}', level=2) From fb96203bba8e3928c009cd11ec0bbc9ad296a35f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:23:39 +0800 Subject: [PATCH 11/18] Upd: Change load complete message for auto updating --- webapp/packages/main/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/main/src/index.ts b/webapp/packages/main/src/index.ts index 3ad98a71e..ce2f89649 100644 --- a/webapp/packages/main/src/index.ts +++ b/webapp/packages/main/src/index.ts @@ -165,7 +165,7 @@ alas.on('stderr', function (message: string) { * Or backend has started already * `[Errno 10048] error while attempting to bind on address ('0.0.0.0', 22267): ` */ - if (message.includes('running on') || message.includes('bind on address')) { + if (message.includes('Application startup complete') || message.includes('bind on address')) { alas.removeAllListeners('stderr'); loadURL() } From ec4fec4f6d53331adec38ea8bfa8b60e78af87ff Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Mon, 10 Jan 2022 23:54:02 +0800 Subject: [PATCH 12/18] Opt: Remove STAGE_SHOWN_WAIT for faster --- module/campaign/campaign_ui.py | 10 +++++----- module/ui/ui.py | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/module/campaign/campaign_ui.py b/module/campaign/campaign_ui.py index 6296e4a4c..58adc9def 100644 --- a/module/campaign/campaign_ui.py +++ b/module/campaign/campaign_ui.py @@ -2,10 +2,10 @@ from module.campaign.assets import * from module.campaign.campaign_ocr import CampaignOcr from module.exception import CampaignNameError, ScriptEnd from module.logger import logger +from module.ui.assets import CAMPAIGN_CHECK from module.ui.switch import Switch -from module.ui.ui import UI, page_campaign, page_event, page_sp +from module.ui.ui import UI -STAGE_SHOWN_WAIT = (1, 1.2) MODE_SWITCH_1 = Switch('Mode_switch_1', offset=(30, 10)) MODE_SWITCH_1.add_status('normal', SWITCH_1_NORMAL) MODE_SWITCH_1.add_status('hard', SWITCH_1_HARD) @@ -27,7 +27,7 @@ class CampaignUI(UI, CampaignOcr): # A tricky way to use ui_ensure_index. self.ui_ensure_index(index, letter=self.get_chapter_index, prev_button=CHAPTER_PREV, next_button=CHAPTER_NEXT, - fast=True, skip_first_screenshot=True, step_sleep=STAGE_SHOWN_WAIT, finish_sleep=0) + fast=True, skip_first_screenshot=True, finish_sleep=0) def campaign_ensure_mode(self, mode='normal'): """ @@ -85,7 +85,7 @@ class CampaignUI(UI, CampaignOcr): chapter, _ = self._campaign_separate_name(name) if chapter.isdigit(): - self.ui_goto(page_campaign) + self.ui_goto_campaign() self.campaign_ensure_mode('normal') self.campaign_ensure_chapter(index=chapter) if mode == 'hard': @@ -131,4 +131,4 @@ class CampaignUI(UI, CampaignOcr): Returns: bool: If any commission finished. """ - return self.appear(page_campaign.check_button, offset=(20, 20)) and self.appear(COMMISSION_NOTICE_AT_CAMPAIGN) + return self.appear(CAMPAIGN_CHECK, offset=(20, 20)) and self.appear(COMMISSION_NOTICE_AT_CAMPAIGN) diff --git a/module/ui/ui.py b/module/ui/ui.py index 7ebc4d9db..5ac684e4f 100644 --- a/module/ui/ui.py +++ b/module/ui/ui.py @@ -211,6 +211,9 @@ class UI(InfoHandler): def ui_goto_main(self): return self.ui_ensure(destination=page_main) + def ui_goto_campaign(self): + return self.ui_ensure(destination=page_campaign) + def ui_goto_event(self): return self.ui_ensure(destination=page_event) From 2443a022f77b3e674b82ea711e4a02aa024417c1 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 00:36:49 +0800 Subject: [PATCH 13/18] Fix: TooManyClickError on clicking MODE_SWITCH_1 when going to hard mode Loop in switch to normal mode, switch to hard mode, ensure chapter but no stage found, switch to normal mode --- module/campaign/campaign_ocr.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/module/campaign/campaign_ocr.py b/module/campaign/campaign_ocr.py index e8511a2e4..b85cdd19d 100644 --- a/module/campaign/campaign_ocr.py +++ b/module/campaign/campaign_ocr.py @@ -2,6 +2,7 @@ import collections from module.base.base import ModuleBase from module.base.decorator import Config +from module.base.timer import Timer from module.base.utils import * from module.exception import CampaignNameError from module.logger import logger @@ -230,9 +231,17 @@ class CampaignOcr(ModuleBase): Returns: int: Chapter index. """ - try: - self._get_stage_name(image) - except IndexError: - raise CampaignNameError + timeout = Timer(2, count=4).start() + while 1: + if timeout.reached(): + raise CampaignNameError + + try: + self._get_stage_name(image) + break + except (IndexError, CampaignNameError): + self.device.screenshot() + image = self.device.image + continue return self._campaign_get_chapter_index(self.campaign_chapter) From 1a0ca6724326fbe0e653b51034dd0c474b8928ce Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 14:18:15 +0800 Subject: [PATCH 14/18] Fix: Research preset series_4_blueprint_only --- module/research/preset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/module/research/preset.py b/module/research/preset.py index afcdbee8c..ecaab1919 100644 --- a/module/research/preset.py +++ b/module/research/preset.py @@ -7,10 +7,10 @@ DICT_FILTER_PRESET = { > S4-G2.5 > S4-PRY5 > S4-PRY8 > 1.5 > 2 > S4-Q4 > 2.5 > 4 > 5 > S4-C6 > S4-C8 > 6 > 8 > S4-C12 > 12""", 'series_4_blueprint_only': """ - S4-DR0.5 > S4-PRY0.5 > S4-H0.5 > S4-DR8 > S4-DR2.5 > S4-DR5 > S4-G1.5 > S4-PRY2.5 - > S4-Q0.5 > !4-0.5 > S4-G2.5 > !4-1 > S4-Q1 > reset > S4-G4 > S4-PRY5 - > !4-1.5 > S4-Q2 > !4-2 > !4-5 > S4-PRY8 > !4-2.5 > S4-Q4 > !4-4 - > !4-C6 > S4-C6 > S4-C8 > !4-C8 > S4-C12 > !4-C12""", + S4-DR0.5 > S4-PRY0.5 > S4-H0.5 > S4-DR8 > S4-DR2.5 > S4-DR5 > S4-G1.5 + > S4-PRY2.5 > S4-Q0.5 > 0.5 > S4-G2.5 > S4-Q1 > 1 > reset > S4-G4 + > S4-PRY5 > 1.5 > S4-Q2 > 2 > S4-PRY8 > 2.5 > S4-Q4 > 4 > 5 > S4-C6 + > 6 > S4-C8 > 8 > S4-C12 > 12""", 'series_4_tenrai_only': """ S4-Q0.5 > S4-PRY0.5 > S4-DR0.5 > S4-Q4 > S4-Q1 > S4-Q2 > S4-H0.5 > 0.5 > S4-G4 > S4-G1.5 > 1 > S4-DR2.5 > S4-PRY2.5 > reset > S4-G2.5 > 1.5 From 9360601e8e5f40c4cd22f704327bbfbca81f095f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 20:58:16 +0800 Subject: [PATCH 15/18] Fix: Typo in os_map_goto_globe() --- module/os/globe_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/os/globe_operation.py b/module/os/globe_operation.py index 24596d135..5aa969529 100644 --- a/module/os/globe_operation.py +++ b/module/os/globe_operation.py @@ -246,7 +246,7 @@ class GlobeOperation(ActionPointHandler, MapEventHandler): # Popup: Leaving current zone will terminate meowfficer searching. # Searching reward will be shown after entering another zone. if self.handle_popup_confirm('GOTO_GLOBE'): - return True + continue # End if self.is_in_globe(): From 0413c747a25c62cff5f94712411bb12e7c1d0ddb Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 21:02:00 +0800 Subject: [PATCH 16/18] Opt: Use callback instead of timeout when closing window --- webapp/packages/main/src/index.ts | 5 +++-- webapp/packages/main/src/pyshell.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/webapp/packages/main/src/index.ts b/webapp/packages/main/src/index.ts index ce2f89649..f80aff795 100644 --- a/webapp/packages/main/src/index.ts +++ b/webapp/packages/main/src/index.ts @@ -102,8 +102,9 @@ const createWindow = async () => { mainWindow?.isMaximized() ? mainWindow?.restore() : mainWindow?.maximize(); }); ipcMain.on('window-close', function () { - alas.kill(); - setTimeout(() => mainWindow?.close(), 500); // Wait taskkill to finish + alas.kill(function () { + mainWindow?.close(); + }) }); // Tray diff --git a/webapp/packages/main/src/pyshell.ts b/webapp/packages/main/src/pyshell.ts index ad9d12cad..0c7bfb567 100644 --- a/webapp/packages/main/src/pyshell.ts +++ b/webapp/packages/main/src/pyshell.ts @@ -21,8 +21,8 @@ export class PyShell extends PythonShell { return this; } - kill(): this { - treeKill(this.childProcess.pid, 'SIGTERM'); + kill(callback: (...args: any[]) => void): this { + treeKill(this.childProcess.pid, 'SIGTERM', callback); return this; } } From 5dc207bb69bddff06433973a9471b7d20f9288c0 Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 21:09:04 +0800 Subject: [PATCH 17/18] Del: Modifying CombatScreenshotInterval is not allowed anymore --- module/config/argument/args.json | 2 +- module/config/argument/override.yaml | 3 +++ module/device/screenshot.py | 7 ++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 70b386752..beee0b70b 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -54,7 +54,7 @@ }, "Optimization": { "CombatScreenshotInterval": { - "type": "input", + "type": "disable", "value": 1.0 }, "TaskHoardingDuration": { diff --git a/module/config/argument/override.yaml b/module/config/argument/override.yaml index 91858f367..496152f25 100644 --- a/module/config/argument/override.yaml +++ b/module/config/argument/override.yaml @@ -4,6 +4,9 @@ # ==================== Alas ==================== +Alas: + Optimization: + CombatScreenshotInterval: 1.0 Restart: Scheduler: SuccessInterval: 0 diff --git a/module/device/screenshot.py b/module/device/screenshot.py index 7d8591f67..2d1136e1d 100644 --- a/module/device/screenshot.py +++ b/module/device/screenshot.py @@ -120,9 +120,6 @@ class Screenshot(AScreenCap): def screenshot_interval_set(self, interval): interval = max(interval, 0.1) if interval != self._screenshot_interval_timer.limit: - if self.config.Campaign_UseAutoSearch: - interval = min(interval, 1.0) - logger.info(f'Screenshot interval set to {interval}s, limited to 1.0s in auto search') - else: - logger.info(f'Screenshot interval set to {interval}s') + interval = min(interval, 1.0) + logger.info(f'Screenshot interval set to {interval}s') self._screenshot_interval_timer.limit = interval From e5220772779a16a7204c1323a2eb94366b8d6d9d Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:14:19 +0800 Subject: [PATCH 18/18] Add: Handle U522 skill --- module/map/fleet.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/module/map/fleet.py b/module/map/fleet.py index 8ccd677aa..c9fe24e77 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -1084,6 +1084,9 @@ class Fleet(Camera, AmbushHandler): Args: location (tuple, str, GridInfo): Destination. + Returns: + bool: If submarine moved + Pages: in: IN_MAP out: IN_MAP @@ -1092,15 +1095,28 @@ class Fleet(Camera, AmbushHandler): self.strategy_submarine_move_enter() if self._submarine_goto(location): self.strategy_submarine_move_confirm() + result = True else: self.strategy_submarine_move_cancel() + result = False # Hunt zone view re-enabled by game, after entering sub move mode self.strategy_set_execute(sub_view=False) self.strategy_close() + return result def submarine_move_near_boss(self, boss): + """ + Args: + boss (tuple, str, GridInfo): Destination. + + Returns: + bool: If submarine moved + """ if not (self.is_call_submarine_at_boss and self.map.select(is_submarine_spawn_point=True)): return False + if self.config.Submarine_DistanceToBoss == 'use_U522_skill': + logger.info('Going to use U522 skill, skip moving submarines') + return False boss = location_ensure(boss) logger.info(f'Move submarine near {location2node(boss)}') @@ -1125,10 +1141,15 @@ class Fleet(Camera, AmbushHandler): '1_grid_to_boss': 1, '2_grid_to_boss': 2 } - logger.attr('Distance to boss', self.config.Submarine_DistanceToBoss) - near = get_location(distance_dict.get(self.config.Submarine_DistanceToBoss, 0)) + distance_to_boss = distance_dict.get(self.config.Submarine_DistanceToBoss, 0) + logger.attr('Distance to boss', distance_to_boss) - self.find_path_initial() - - logger.info(f'Move submarine to {location2node(near)}') - self.submarine_goto(near) + if np.sum(np.abs(np.subtract(self.fleet_submarine_location, boss))) <= distance_to_boss: + logger.info('Boss is already in hunting zone') + self.find_path_initial() + return False + else: + near = get_location(distance_to_boss) + self.find_path_initial() + logger.info(f'Move submarine to {location2node(near)}') + return self.submarine_goto(near)