├── icon.png ├── readme └── img.png ├── requirements.txt ├── i18n ├── en_US │ └── LC_MESSAGES │ │ ├── ok.mo │ │ └── ok.po └── zh_CN │ └── LC_MESSAGES │ ├── ok.mo │ └── ok.po ├── main.py ├── src ├── __init__.py ├── char │ ├── Baizhi.py │ ├── Calcharo.py │ ├── Taoqi.py │ ├── Sanhua.py │ ├── HavocRover.py │ ├── Yuanwu.py │ ├── Chixia.py │ ├── Verina.py │ ├── Danjin.py │ ├── Jianxin.py │ ├── Yinlin.py │ ├── Changli.py │ ├── CharFactory.py │ ├── Encore.py │ ├── CharSkillButton.py │ ├── Jinhsi.py │ └── BaseChar.py ├── task │ ├── AutoCombatTask.py │ ├── AutoPickTask.py │ ├── SkipDialogTask.py │ ├── FarmEchoTask.py │ ├── FarmWorldBossTask.py │ └── BaseCombatTask.py └── combat │ └── CombatCheck.py ├── assets ├── 4_png.rf.180bb3f2bed38134b1d0bba90c3fcdb0.png ├── e521850f-00_37_38_458841_Encore_liberation_0-274_original_png.rf.e7703de2285a6b2c70567e891804c760.png ├── 01304a45-10_34_04_580378_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_WnYCyUT_png.rf.7b5dfc4585195e1d846574b715468897.png ├── 0ab28c72-00_54_43_431654_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_usg73Cm_png.rf.124dc89f9b3816ecb0ce5a97c95475dc.png ├── 154eb284-17_01_16_406889_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_6yGu616_png.rf.12ebc1f4e7e5208bcbf2c7f878f691aa.png ├── 221458a6-13_25_04_962031_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_XYXLS23_png.rf.281b46703c1e3f6bd436750f6aa82cf2.png ├── 3b991d47-10_18_37_465122_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3Hw8AdB_png.rf.7b75bb60b5b041f3c6f36e735ca3bc19.png ├── 55617870-00_55_49_536245_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NSeTZ2S_png.rf.5947b06a7caca9b89c2e06ec7ceab8fa.png ├── 5841f84c-17_04_26_738022_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_TIDxIl1_png.rf.97a22c8e01f1fd88debc20c4c903b8b9.png ├── 64425cad-10_44_19_536997_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_BLAxtp1_png.rf.d37538db2b2d15d6aad459dd08bb348d.png ├── 934f0ca6-17_53_47_840123_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_aJQVINN_png.rf.a107256237035eec3be4125f726b8b73.png ├── 9beed513-16_15_23_246388_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_a4tQhDg_png.rf.4e79b52b95ad46f47bc470f069a65b29.png ├── 9dd77a5d-16_57_31_014010_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_RVvCfQe_png.rf.e8397fbd133d485459bb8c30fc37353c.png ├── 9ed8b373-00_57_50_813872_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3jLoRJh_png.rf.170fe2ae11c1d8058d8d3e4b2361357a.png ├── 9f04db98-16_20_58_825243_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_VuLpBVc_png.rf.a16ce4854c42840f44a6c0a04d44c83c.png ├── cef1b3dd-00_55_22_640294_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NtclVno_png.rf.0eb178e9974bb9135567f91780337d55.png ├── WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_133336_1_False_original_png.rf.a2b55fa3583fef2081d49205ae9c643b.png ├── WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_1841174_1_False_original-1-_png.rf.e6c4a5734163badfbfa76780248cd2f8.png ├── WindowsGraphicsCaptureMethod_1920x1080_title_None_Client-Win64-Shipping-exe_1920x1080_329010_1_False_original_png.rf.b6f619b8fe6f0e1e8a0993a5e61e2801.png ├── WindowsGraphicsCaptureMethod_2560x1440_title_None_Client-Win64-Shipping-exe_3840x2160_465786_1_False_original_png.rf.015495d17edfa6c8bf29047cfcc19d6d.png ├── WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_10361910_1_False_original_png.rf.244ee2bd0b4de93caac3066adaae0b75.png ├── WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_12847792_1_False_original_png.rf.abeaa08198ff5a77fb70340952ba23f3.png ├── WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_1577828_1_False_original_png.rf.3ed83cbce453e93e1027f37184cff0a5.png ├── WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_4720836_1_False_original_png.rf.935bb51188f6bc516efc5a57b52fe8ad.png ├── WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_7476412_1_False_original_png.rf.a21fbbbefc2fa48f56917d19a6a443b2.png ├── 17_11_07_938917_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_396094_1_False_original_png.rf.f921e6fb49de02d7ef17e7f5f6a976c4.png └── _annotations.coco.json ├── requirements-dev.txt ├── main_debug.py ├── main_debug_console.py ├── .github ├── workflows │ ├── needs-reply.yml │ ├── needs-reply-remove.yml │ └── build.yml └── ISSUE_TEMPLATE │ └── 报告bug-.md ├── .gitignore ├── README.md ├── config.py ├── main_debug.spec ├── main.spec └── LICENSE.txt /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/icon.png -------------------------------------------------------------------------------- /readme/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/readme/img.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/requirements.txt -------------------------------------------------------------------------------- /i18n/en_US/LC_MESSAGES/ok.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/i18n/en_US/LC_MESSAGES/ok.mo -------------------------------------------------------------------------------- /i18n/zh_CN/LC_MESSAGES/ok.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/i18n/zh_CN/LC_MESSAGES/ok.mo -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | from ok.OK import OK 3 | 4 | config = config 5 | ok = OK(config) 6 | ok.start() 7 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | text_white_color = { 2 | 'r': (255, 255), # Red range 3 | 'g': (255, 255), # Green range 4 | 'b': (255, 255) # Blue range 5 | } 6 | -------------------------------------------------------------------------------- /assets/4_png.rf.180bb3f2bed38134b1d0bba90c3fcdb0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/4_png.rf.180bb3f2bed38134b1d0bba90c3fcdb0.png -------------------------------------------------------------------------------- /src/char/Baizhi.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Baizhi(BaseChar): 5 | 6 | def count_base_priority(self): 7 | return - 1 8 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | adbutils 2 | numpy 3 | PyDirectInput 4 | pywin32 5 | typing-extensions 6 | opencv-python 7 | PySide6 8 | PySide6-Fluent-Widgets 9 | psutil 10 | py7zr 11 | 12 | rapidocr_openvino 13 | -------------------------------------------------------------------------------- /main_debug.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | from ok.OK import OK 3 | 4 | config = config 5 | config['debug'] = True 6 | # config['click_screenshots_folder'] = "click_screenshots" # debug用 点击后截图文件夹] 7 | ok = OK(config) 8 | ok.start() 9 | -------------------------------------------------------------------------------- /main_debug_console.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | from ok.OK import OK 3 | 4 | config = config 5 | config['debug'] = True 6 | config['use_gui'] = False 7 | config['onetime_tasks'][1].enable() 8 | ok = OK(config) 9 | ok.start() 10 | -------------------------------------------------------------------------------- /assets/e521850f-00_37_38_458841_Encore_liberation_0-274_original_png.rf.e7703de2285a6b2c70567e891804c760.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/e521850f-00_37_38_458841_Encore_liberation_0-274_original_png.rf.e7703de2285a6b2c70567e891804c760.png -------------------------------------------------------------------------------- /assets/01304a45-10_34_04_580378_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_WnYCyUT_png.rf.7b5dfc4585195e1d846574b715468897.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/01304a45-10_34_04_580378_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_WnYCyUT_png.rf.7b5dfc4585195e1d846574b715468897.png -------------------------------------------------------------------------------- /assets/0ab28c72-00_54_43_431654_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_usg73Cm_png.rf.124dc89f9b3816ecb0ce5a97c95475dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/0ab28c72-00_54_43_431654_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_usg73Cm_png.rf.124dc89f9b3816ecb0ce5a97c95475dc.png -------------------------------------------------------------------------------- /assets/154eb284-17_01_16_406889_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_6yGu616_png.rf.12ebc1f4e7e5208bcbf2c7f878f691aa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/154eb284-17_01_16_406889_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_6yGu616_png.rf.12ebc1f4e7e5208bcbf2c7f878f691aa.png -------------------------------------------------------------------------------- /assets/221458a6-13_25_04_962031_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_XYXLS23_png.rf.281b46703c1e3f6bd436750f6aa82cf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/221458a6-13_25_04_962031_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_XYXLS23_png.rf.281b46703c1e3f6bd436750f6aa82cf2.png -------------------------------------------------------------------------------- /assets/3b991d47-10_18_37_465122_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3Hw8AdB_png.rf.7b75bb60b5b041f3c6f36e735ca3bc19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/3b991d47-10_18_37_465122_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3Hw8AdB_png.rf.7b75bb60b5b041f3c6f36e735ca3bc19.png -------------------------------------------------------------------------------- /assets/55617870-00_55_49_536245_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NSeTZ2S_png.rf.5947b06a7caca9b89c2e06ec7ceab8fa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/55617870-00_55_49_536245_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NSeTZ2S_png.rf.5947b06a7caca9b89c2e06ec7ceab8fa.png -------------------------------------------------------------------------------- /assets/5841f84c-17_04_26_738022_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_TIDxIl1_png.rf.97a22c8e01f1fd88debc20c4c903b8b9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/5841f84c-17_04_26_738022_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_TIDxIl1_png.rf.97a22c8e01f1fd88debc20c4c903b8b9.png -------------------------------------------------------------------------------- /assets/64425cad-10_44_19_536997_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_BLAxtp1_png.rf.d37538db2b2d15d6aad459dd08bb348d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/64425cad-10_44_19_536997_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win_BLAxtp1_png.rf.d37538db2b2d15d6aad459dd08bb348d.png -------------------------------------------------------------------------------- /assets/934f0ca6-17_53_47_840123_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_aJQVINN_png.rf.a107256237035eec3be4125f726b8b73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/934f0ca6-17_53_47_840123_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_aJQVINN_png.rf.a107256237035eec3be4125f726b8b73.png -------------------------------------------------------------------------------- /assets/9beed513-16_15_23_246388_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_a4tQhDg_png.rf.4e79b52b95ad46f47bc470f069a65b29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/9beed513-16_15_23_246388_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_a4tQhDg_png.rf.4e79b52b95ad46f47bc470f069a65b29.png -------------------------------------------------------------------------------- /assets/9dd77a5d-16_57_31_014010_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_RVvCfQe_png.rf.e8397fbd133d485459bb8c30fc37353c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/9dd77a5d-16_57_31_014010_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_RVvCfQe_png.rf.e8397fbd133d485459bb8c30fc37353c.png -------------------------------------------------------------------------------- /assets/9ed8b373-00_57_50_813872_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3jLoRJh_png.rf.170fe2ae11c1d8058d8d3e4b2361357a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/9ed8b373-00_57_50_813872_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_3jLoRJh_png.rf.170fe2ae11c1d8058d8d3e4b2361357a.png -------------------------------------------------------------------------------- /assets/9f04db98-16_20_58_825243_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_VuLpBVc_png.rf.a16ce4854c42840f44a6c0a04d44c83c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/9f04db98-16_20_58_825243_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_VuLpBVc_png.rf.a16ce4854c42840f44a6c0a04d44c83c.png -------------------------------------------------------------------------------- /assets/cef1b3dd-00_55_22_640294_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NtclVno_png.rf.0eb178e9974bb9135567f91780337d55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/cef1b3dd-00_55_22_640294_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_NtclVno_png.rf.0eb178e9974bb9135567f91780337d55.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_133336_1_False_original_png.rf.a2b55fa3583fef2081d49205ae9c643b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_133336_1_False_original_png.rf.a2b55fa3583fef2081d49205ae9c643b.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_1841174_1_False_original-1-_png.rf.e6c4a5734163badfbfa76780248cd2f8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_1841174_1_False_original-1-_png.rf.e6c4a5734163badfbfa76780248cd2f8.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_1920x1080_title_None_Client-Win64-Shipping-exe_1920x1080_329010_1_False_original_png.rf.b6f619b8fe6f0e1e8a0993a5e61e2801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_1920x1080_title_None_Client-Win64-Shipping-exe_1920x1080_329010_1_False_original_png.rf.b6f619b8fe6f0e1e8a0993a5e61e2801.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_2560x1440_title_None_Client-Win64-Shipping-exe_3840x2160_465786_1_False_original_png.rf.015495d17edfa6c8bf29047cfcc19d6d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_2560x1440_title_None_Client-Win64-Shipping-exe_3840x2160_465786_1_False_original_png.rf.015495d17edfa6c8bf29047cfcc19d6d.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_10361910_1_False_original_png.rf.244ee2bd0b4de93caac3066adaae0b75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_10361910_1_False_original_png.rf.244ee2bd0b4de93caac3066adaae0b75.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_12847792_1_False_original_png.rf.abeaa08198ff5a77fb70340952ba23f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_12847792_1_False_original_png.rf.abeaa08198ff5a77fb70340952ba23f3.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_1577828_1_False_original_png.rf.3ed83cbce453e93e1027f37184cff0a5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_1577828_1_False_original_png.rf.3ed83cbce453e93e1027f37184cff0a5.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_4720836_1_False_original_png.rf.935bb51188f6bc516efc5a57b52fe8ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_4720836_1_False_original_png.rf.935bb51188f6bc516efc5a57b52fe8ad.png -------------------------------------------------------------------------------- /assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_7476412_1_False_original_png.rf.a21fbbbefc2fa48f56917d19a6a443b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/WindowsGraphicsCaptureMethod_3840x2160_title_None_Client-Win64-Shipping-exe_3840x2160_7476412_1_False_original_png.rf.a21fbbbefc2fa48f56917d19a6a443b2.png -------------------------------------------------------------------------------- /assets/17_11_07_938917_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_396094_1_False_original_png.rf.f921e6fb49de02d7ef17e7f5f6a976c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/ok-wuthering-waves/master/assets/17_11_07_938917_WindowsGraphicsCaptureMethod_0x0_title_None_Client-Win64-Shipping-exe_3840x2160_396094_1_False_original_png.rf.f921e6fb49de02d7ef17e7f5f6a976c4.png -------------------------------------------------------------------------------- /.github/workflows/needs-reply.yml: -------------------------------------------------------------------------------- 1 | name: Close old issues that need reply 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Close old issues that need reply 12 | uses: dwieeb/needs-reply@v2 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-label: needs-reply -------------------------------------------------------------------------------- /src/char/Calcharo.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Calcharo(BaseChar): 5 | def do_perform(self): 6 | if self.has_intro: 7 | self.logger.debug('Calcharo wait intro animation') 8 | self.sleep(1) 9 | self.task.wait_in_team_and_world(time_out=3, raise_if_not_found=False) 10 | self.check_combat() 11 | super().do_perform() 12 | -------------------------------------------------------------------------------- /src/char/Taoqi.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Taoqi(BaseChar): 5 | def do_perform(self): 6 | if self.has_intro: 7 | self.wait_down() 8 | self.continues_normal_attack(2.5) 9 | else: 10 | self.click_liberation() 11 | self.click_resonance() 12 | self.click_echo(sleep_time=0.1) 13 | self.switch_next_char() 14 | -------------------------------------------------------------------------------- /src/char/Sanhua.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Sanhua(BaseChar): 5 | def do_perform(self): 6 | self.click_liberation() 7 | if self.click_resonance()[0]: 8 | return self.switch_next_char() 9 | if self.click_echo(): 10 | return self.switch_next_char() 11 | 12 | self.task.mouse_down() 13 | self.sleep(.7) 14 | self.task.mouse_up() 15 | self.sleep(0.3) 16 | self.switch_next_char() 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/报告bug-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug ' 3 | about: 遇到的问题 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: ok-oldking 7 | 8 | --- 9 | 10 | 描述错误: 11 | [请清晰简洁地描述错误是什么] 12 | 13 | 复现步骤 按照以下步骤复现行为: 14 | 15 | 1. 点击 ‘…’ 16 | 2. 向下滚动到 ‘…’ 17 | 3. 看到错误 18 | 19 | 截图 : 20 | 如果适用,添加截图以帮助解释您的问题。 21 | 22 | 脚本软件版本: 23 | [如]1.1.1 24 | 25 | windows操作系统: 26 | [例如] Windows 11 Pro 23H2 22631.3593 27 | 28 | 模拟器版本: 29 | [如使用模拟器]MuMu模拟器12 V3.8.25 (2927) 30 | 31 | 上传日志: 32 | 将脚本安装目录的logs,screenshots,config,click_screenshots打包上传附件 33 | -------------------------------------------------------------------------------- /src/char/HavocRover.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class HavocRover(BaseChar): 5 | def do_perform(self): 6 | if self.is_forte_full() and self.liberation_available(): 7 | self.logger.info(f'forte_full, and liberation_available, heavy attack') 8 | self.wait_down() 9 | self.heavy_attack() 10 | self.sleep(0.4) 11 | self.click_liberation() 12 | if not self.click_resonance()[0]: 13 | self.click_echo() 14 | self.switch_next_char() 15 | -------------------------------------------------------------------------------- /src/char/Yuanwu.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Yuanwu(BaseChar): 5 | 6 | def count_resonance_priority(self): 7 | return 0 8 | 9 | def count_echo_priority(self): 10 | return 0 11 | 12 | def count_base_priority(self): 13 | return -1 14 | 15 | def do_perform(self): 16 | self.click_liberation(con_less_than=1) 17 | if self.is_forte_full(): 18 | self.send_resonance_key(down_time=0.6, post_sleep=0.2) 19 | self.click_echo() 20 | self.switch_next_char() 21 | -------------------------------------------------------------------------------- /src/char/Chixia.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Chixia(BaseChar): 5 | 6 | def __init__(self, *args): 7 | super().__init__(*args) 8 | self.bullets = 0 9 | 10 | # def do_perform(self): 11 | # if self.resonance_available(): 12 | # if self.bullets > 35: 13 | # self.task.send_key_down(self.get_resonance_key()) 14 | # self.sleep(3) 15 | # self.click 16 | # else: 17 | # self.task.send_key_up(self.get_resonance_key()) 18 | # self.switch_next_char() 19 | -------------------------------------------------------------------------------- /src/char/Verina.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Verina(BaseChar): 5 | 6 | def do_perform(self): 7 | self.click_liberation() 8 | if self.flying(): 9 | return self.switch_next_char() 10 | if self.click_resonance(send_click=False)[0]: 11 | return self.switch_next_char() 12 | if self.click_echo(): 13 | self.heavy_attack() 14 | return self.switch_next_char() 15 | self.heavy_attack() 16 | self.switch_next_char() 17 | 18 | def count_base_priority(self): 19 | return - 1 20 | -------------------------------------------------------------------------------- /src/char/Danjin.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Danjin(BaseChar): 5 | 6 | def __init__(self, *args): 7 | super().__init__(*args) 8 | # self.bullets = 0 9 | 10 | def count_resonance_priority(self): 11 | return 0 12 | 13 | def do_perform(self): 14 | self.click_liberation() 15 | if self.is_forte_full(): 16 | self.heavy_attack() 17 | self.sleep(0.2) 18 | elif self.click_echo(): 19 | pass 20 | else: 21 | self.task.send_key(self.get_resonance_key()) 22 | self.switch_next_char() 23 | -------------------------------------------------------------------------------- /src/char/Jianxin.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Jianxin(BaseChar): 5 | def do_perform(self): 6 | if self.has_intro: 7 | self.sleep(0.8) 8 | # if self.liberation_available(): 9 | # self.click_liberation() 10 | # self.sleep(2) 11 | if self.is_forte_full(): 12 | self.task.mouse_down() 13 | self.sleep(5.6) 14 | self.task.mouse_up() 15 | if self.resonance_available(): 16 | self.click_resonance() 17 | if self.echo_available(): 18 | self.sleep(0.3) 19 | self.click_echo() 20 | self.sleep(0.3) 21 | self.switch_next_char() 22 | -------------------------------------------------------------------------------- /.github/workflows/needs-reply-remove.yml: -------------------------------------------------------------------------------- 1 | name: Remove needs-reply label 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: | 12 | github.event.comment.author_association != 'OWNER' && 13 | github.event.comment.author_association != 'COLLABORATOR' 14 | steps: 15 | - name: Remove needs-reply label 16 | uses: octokit/request-action@v2.x 17 | continue-on-error: true 18 | with: 19 | route: DELETE /repos/:repository/issues/:issue/labels/:label 20 | repository: ${{ github.repository }} 21 | issue: ${{ github.event.issue.number }} 22 | label: needs-reply 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | updates/ 2 | tesseract 3 | models 4 | fonts 5 | _internal/ 6 | # Byte-compiled / optimized / DLL files 7 | .idea/ 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | configs/ 12 | ok/ 13 | ok 14 | *.exe 15 | md5.txt 16 | update.bat 17 | click_screenshots/ 18 | test_scripts/ 19 | screenshots/ 20 | thread_dumps.txt 21 | # C extensions 22 | *.so 23 | *.bat 24 | 25 | # Distribution / packaging 26 | .Python 27 | test/ 28 | logs/ 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | venv/ 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | autohelper 57 | working_images/ 58 | -------------------------------------------------------------------------------- /src/task/AutoCombatTask.py: -------------------------------------------------------------------------------- 1 | from ok.logging.Logger import get_logger 2 | from ok.task.TriggerTask import TriggerTask 3 | from src.task.BaseCombatTask import BaseCombatTask, NotInCombatException 4 | 5 | logger = get_logger(__name__) 6 | 7 | 8 | class AutoCombatTask(BaseCombatTask, TriggerTask): 9 | 10 | def __init__(self): 11 | super().__init__() 12 | self.trigger_interval = 0.2 13 | self.name = "Auto Combat" 14 | self.description = "Enable auto combat in Abyss, Game World etc" 15 | 16 | def run(self): 17 | while self.in_combat(): 18 | try: 19 | logger.debug(f'autocombat loop {self.chars}') 20 | self.get_current_char().perform() 21 | except NotInCombatException as e: 22 | logger.info(f'auto_combat_task_out_of_combat {e}') 23 | if self.debug: 24 | self.screenshot(f'auto_combat_task_out_of_combat {e}') 25 | break 26 | 27 | def trigger(self): 28 | if self.in_combat(): 29 | self.load_chars() 30 | return True 31 | -------------------------------------------------------------------------------- /src/char/Yinlin.py: -------------------------------------------------------------------------------- 1 | from src.char.BaseChar import BaseChar 2 | 3 | 4 | class Yinlin(BaseChar): 5 | def do_perform(self): 6 | if self.has_intro: 7 | self.sleep(0.4) 8 | liberation = self.click_liberation() 9 | if self.is_forte_full(): 10 | if not self.has_intro and not liberation: 11 | self.normal_attack() 12 | self.heavy_attack() 13 | self.sleep(0.4) 14 | elif self.click_resonance(send_click=False)[0]: 15 | self.sleep(0.1) 16 | elif self.echo_available(): 17 | echo_key = self.get_echo_key() 18 | self.sleep(0.1) 19 | self.task.send_key_down(echo_key) 20 | self.sleep(.6) 21 | return self.switch_next_char(post_action=self.echo_post_action) 22 | else: 23 | self.heavy_attack() 24 | self.switch_next_char() 25 | 26 | def count_base_priority(self): 27 | return 2 28 | 29 | def count_forte_priority(self): 30 | return 20 31 | 32 | def count_liberation_priority(self): 33 | return 0 34 | 35 | def count_echo_priority(self): 36 | return 1 37 | 38 | def echo_post_action(self): # hold down the echo for 1 seconds and switch and then release the echo key 39 | self.task.send_key_up(self.get_echo_key()) 40 | self.sleep(0.01) 41 | -------------------------------------------------------------------------------- /src/task/AutoPickTask.py: -------------------------------------------------------------------------------- 1 | from ok.feature.FindFeature import FindFeature 2 | from ok.logging.Logger import get_logger 3 | from ok.task.TriggerTask import TriggerTask 4 | 5 | logger = get_logger(__name__) 6 | 7 | 8 | class AutoPickTask(TriggerTask, FindFeature): 9 | 10 | def __init__(self): 11 | super().__init__() 12 | self.name = "Auto Pick" 13 | self.description = "Auto Pick Flowers in Game World" 14 | 15 | def run(self): 16 | self.send_key('f') 17 | self.sleep(0.2) 18 | self.send_key('f') 19 | self.sleep(0.2) 20 | self.send_key('f') 21 | self.sleep(0.2) 22 | 23 | def trigger(self): 24 | f_search_box = self.get_box_by_name('pick_up_f') 25 | f_search_box = f_search_box.copy(x_offset=-f_search_box.width / 2, 26 | width_offset=f_search_box.width, 27 | height_offset=f_search_box.height * 4, 28 | y_offset=-f_search_box.height / 2, 29 | name='search_dialog') 30 | if f := self.find_one('pick_up_f', box=f_search_box, 31 | threshold=0.8): 32 | dialog_search = f.copy(x_offset=f.width * 2, width_offset=f.width * 2, height_offset=f.height * 2, 33 | y_offset=-f.height, 34 | name='search_dialog') 35 | return self.find_one('dialog_3_dots', box=dialog_search, 36 | threshold=0.8) is None 37 | -------------------------------------------------------------------------------- /src/char/Changli.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from src.char.BaseChar import BaseChar, Priority 4 | 5 | 6 | class Changli(BaseChar): 7 | 8 | def __init__(self, *args): 9 | super().__init__(*args) 10 | self.enhanced_normal = False 11 | self.last_e = 0 12 | 13 | def reset_state(self): 14 | self.enhanced_normal = False 15 | 16 | def do_get_switch_priority(self, current_char: BaseChar, has_intro=False): 17 | if time.time() - self.last_e < 3: 18 | self.logger.info( 19 | f'switch priority MIN because e not finished') 20 | return Priority.MIN 21 | else: 22 | return super().do_get_switch_priority(current_char, has_intro) 23 | 24 | def do_perform(self): 25 | # self.logger.debug( 26 | # f'Encore_perform_{self.has_intro}_{self.echo_available()}_{self.resonance_available()}_{self.liberation_available()}') 27 | if self.has_intro or self.enhanced_normal: 28 | self.normal_attack() 29 | self.sleep(0.5) 30 | self.enhanced_normal = False 31 | if self.is_forte_full(): 32 | self.heavy_attack(0.8) 33 | return self.switch_next_char() 34 | if self.click_liberation(): 35 | self.heavy_attack(0.8) 36 | return self.switch_next_char() 37 | elif self.resonance_available(): 38 | self.send_resonance_key() 39 | self.enhanced_normal = True 40 | self.normal_attack() 41 | elif self.click_echo(1.5): 42 | pass 43 | else: 44 | self.normal_attack() 45 | self.logger.info('Changli nothing is available') 46 | self.switch_next_char() 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  2 | 3 | ## OK-WW 4 | * Automation for Wuthering Waves using Computer Vision, Auto Combat 5 | * 点击[releases](https://github.com/ok-oldking/ok-wuthering-waves/releases), 下载7z压缩包, 解压缩双击运行.exe 6 | * 下载有问题的, 也可加入腾讯QQ频道,[OK-WW](https://pd.qq.com/s/2jhl3oogp) 7 | * QQ水群,只聊游戏, 不下载不讨论软件: [970523295](https://qm.qq.com/q/qMezq2IDGU) 8 | 9 | 10 | ### 有多强? 11 | 12 | 1. 4K分辨率流畅运行,支持所有16:9分辨率,1600x900以上, 1280x720不支持是因为鸣潮bug, 它的1280x720并不是1280x720 13 | 2. 可后台运行,可窗口化,可全屏,屏幕缩放比例无要求 14 | 3. 自动战斗比大多数玩家手操都强, 深渊可满星, 演示视频: [今汐12秒轴](https://www.bilibili.com/video/BV1Hx4y1t7NP/) 15 | 4. 无需安装Cuda之类, 基本不占用显卡资源, 性能优化到支持自动战斗10毫秒左右的响应时间 16 | 5. 可高度自定义角色出招逻辑(动态合轴) [角色列表](src/char) 17 | 18 | ### 出现问题请检查 19 | 20 | 1. 关闭windows HDR, 护眼低蓝光模式, 游戏使用默认亮度, 关闭显卡滤镜,等一切改变游戏颜色的功能 21 | 2. 所有角色必须装备主声骸, 暂时不支持卡卡罗, 会跳出战斗 22 | 3. 把下载目录和解压目录, 添加到杀毒软件白名单. 23 | 4. 不要直接解压在QQ下载文件夹里运行, 不要放中文目录 24 | 5. OK-WW没更新最新版的, 更新最新版 25 | 6. 如果手动改过鸣潮或者鸣潮启动器的DPI设置, 重置 26 | 7. 最好升级Win10最新版(至少版本20348以上)或者Win11 27 | 28 | Demonstration: [https://youtu.be/N32I1aMfdqQ](https://youtu.be/N32I1aMfdqQ) 29 | 30 | * Farm Boss Echo (Dreamless, Jue and World Bosses) 31 | * Auto Combat (Beats 90% players for [Fully Supported Characters](src/char)) 32 | * Auto Skip Dialogs in Quests 33 | * Supports All Game Languages 34 |  35 | 36 | ### How to Run 37 | 38 | * Download the 7z from [releases](https://github.com/ok-oldking/ok-wuthering-waves/releases), extract and run the exe 39 | * May need to add the app folder to Windows Defender white list. 40 | * Game must be a 16:9 ratio like 1920x1080, 3840x2160, lowest supported resolution is 1600*900 41 | * Can run while game is in background, but not minimized 42 | 43 | ### Development 44 | 45 | use Python 3.11, other versions might work but not tested 46 | 47 | ``` 48 | pip install -r requirements.txt 49 | python main_debug.py 50 | ``` 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /i18n/en_US/LC_MESSAGES/ok.po: -------------------------------------------------------------------------------- 1 | # 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: 1.0\n" 5 | "Report-Msgid-Bugs-To: you@example.com\n" 6 | "Last-Translator: you@example.com\n" 7 | "Language-Team: English\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | 12 | msgid "(1-6) Important, Choose which level to farm, lower levels might not produce a echo" 13 | msgstr "" 14 | 15 | msgid "Boss1" 16 | msgstr "" 17 | 18 | msgid "Boss2" 19 | msgstr "" 20 | 21 | msgid "Boss3" 22 | msgstr "" 23 | 24 | msgid "Choose Forward for Dreamless, Backward for Jue" 25 | msgstr "" 26 | 27 | msgid "Click Start at the Entrance(Dreamless, Jue)" 28 | msgstr "" 29 | 30 | msgid "Click Start in Game World" 31 | msgstr "" 32 | 33 | msgid "Echo Key" 34 | msgstr "" 35 | 36 | msgid "Entrance Direction" 37 | msgstr "" 38 | 39 | msgid "Farm Echo in Dungeon" 40 | msgstr "" 41 | 42 | msgid "Farm World Boss(Must Drop a WayPoint on the Boss First)" 43 | msgstr "" 44 | 45 | msgid "Game Hotkey Config" 46 | msgstr "" 47 | 48 | msgid "In Game Hotkey for Skills" 49 | msgstr "" 50 | 51 | msgid "Level" 52 | msgstr "" 53 | 54 | msgid "Liberation Key" 55 | msgstr "" 56 | 57 | msgid "Repeat Farm Count" 58 | msgstr "" 59 | 60 | msgid "Resonance Key" 61 | msgstr "" 62 | 63 | msgid "Auto Combat" 64 | msgstr "" 65 | 66 | msgid "Backward" 67 | msgstr "" 68 | 69 | msgid "Bell-Borne Geochelone" 70 | msgstr "" 71 | 72 | msgid "Crownless" 73 | msgstr "" 74 | 75 | msgid "Enable auto combat in Abyss, Game World etc" 76 | msgstr "" 77 | 78 | msgid "Feilian Beringal" 79 | msgstr "" 80 | 81 | msgid "Forward" 82 | msgstr "" 83 | 84 | msgid "Impermanence Heron" 85 | msgstr "" 86 | 87 | msgid "Inferno Rider" 88 | msgstr "" 89 | 90 | msgid "Lampylumen Myriad" 91 | msgstr "" 92 | 93 | msgid "Mech Abomination" 94 | msgstr "" 95 | 96 | msgid "Mourning Aix" 97 | msgstr "" 98 | 99 | msgid "N/A" 100 | msgstr "" 101 | 102 | msgid "Skip Dialog during Quests" 103 | msgstr "" 104 | 105 | msgid "Tempest Mephis" 106 | msgstr "" 107 | 108 | msgid "Thundering Mephis" 109 | msgstr "" 110 | 111 | msgid "Combat Count" 112 | msgstr "" 113 | 114 | msgid "Echo Count" 115 | msgstr "" 116 | 117 | msgid "Log" 118 | msgstr "" 119 | 120 | msgid "Auto Pick" 121 | msgstr "" 122 | 123 | msgid "Auto Pick Flowers in Game World" 124 | msgstr "" 125 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Windows Executable 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | build: 11 | name: Build exe with PyInstaller 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | name: Checkout code 16 | with: 17 | fetch-depth: 0 # Important: fetch all history for all tags and branches 18 | 19 | - name: Get Changes between Tags 20 | id: changes 21 | uses: simbo/changes-between-tags-action@v1 22 | with: 23 | validate-tag: false 24 | 25 | - name: Get tag name 26 | id: tagName 27 | uses: olegtarasov/get-tag@v2.1.3 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: '3.11' # Use the version of Python you need 33 | 34 | - name: Install Dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install pyinstaller # Add other dependencies if needed 38 | pip install -r requirements.txt 39 | 40 | - name: Build Executable 41 | run: | 42 | echo "tag: ${{ steps.changes.outputs.tag }}" 43 | echo "changes: ${{ steps.changes.outputs.changes }}" 44 | echo ${{ github.sha }} > Release.txt 45 | (Get-Content config.py) -replace 'version = "v\d+\.\d+\.\d+"', 'version = "${{ steps.tagName.outputs.tag }}"' | Set-Content config.py 46 | pyinstaller main.spec 47 | python -m ok.update.gen_md5 .\dist\bundle 48 | mv dist/bundle ok-ww 49 | 7z a -t7z -r "ok-ww-release-${{ steps.tagName.outputs.tag }}.7z" "ok-ww" 50 | 51 | shell: pwsh 52 | 53 | - name: Create Release 54 | id: create_release 55 | uses: actions/create-release@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | tag_name: ${{ github.ref }} 60 | release_name: Release ${{ github.ref }} 61 | body: | 62 | Updates: 63 | ${{ steps.changes.outputs.changes }} 64 | draft: false 65 | prerelease: true 66 | 67 | - name: upload-win 68 | uses: actions/upload-release-asset@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | upload_url: ${{ steps.create_release.outputs.upload_url }} 73 | asset_path: ./ok-ww-release-${{ steps.tagName.outputs.tag }}.7z 74 | asset_name: ok-ww-release-${{ steps.tagName.outputs.tag }}.7z 75 | asset_content_type: application/zip 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /i18n/zh_CN/LC_MESSAGES/ok.po: -------------------------------------------------------------------------------- 1 | # 2 | msgid "" 3 | msgstr "" 4 | 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: 1.0\n" 8 | "Report-Msgid-Bugs-To: you@example.com\n" 9 | "Last-Translator: you@example.com\n" 10 | "Language-Team: English\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | 15 | msgid "(1-6) Important, Choose which level to farm, lower levels might not produce a echo" 16 | msgstr "(1-6) 重要, 选择你对应世界等级可以产出声骸的难度, 从上到下为1-6" 17 | 18 | msgid "Boss1" 19 | msgstr "第1个Boss" 20 | 21 | msgid "Boss2" 22 | msgstr "第2个Boss" 23 | 24 | msgid "Boss3" 25 | msgstr "第3个Boss" 26 | 27 | msgid "Choose Forward for Dreamless, Backward for Jue" 28 | msgstr "无妄者选前进, 角选后退" 29 | 30 | msgid "Click Start at the Entrance(Dreamless, Jue)" 31 | msgstr "在入口点击(无妄者, 角)" 32 | 33 | msgid "Click Start in Game World" 34 | msgstr "在游戏世界点击开始" 35 | 36 | msgid "Echo Key" 37 | msgstr "声骸按键(默认q)" 38 | 39 | msgid "Entrance Direction" 40 | msgstr "入口方向" 41 | 42 | msgid "Farm Echo in Dungeon" 43 | msgstr "刷副本Boss声骸" 44 | 45 | msgid "Farm World Boss(Must Drop a WayPoint on the Boss First)" 46 | msgstr "刷大世界4C Boss声骸(先在要刷的Boss脸上放锚点)" 47 | 48 | msgid "Game Hotkey Config" 49 | msgstr "游戏快捷键设置" 50 | 51 | msgid "In Game Hotkey for Skills" 52 | msgstr "声骸, 共鸣技能, 共鸣解放" 53 | 54 | msgid "Level" 55 | msgstr "等级" 56 | 57 | msgid "Liberation Key" 58 | msgstr "共鸣解放快捷键(默认r)" 59 | 60 | msgid "Repeat Farm Count" 61 | msgstr "刷多少次" 62 | 63 | msgid "Resonance Key" 64 | msgstr "共鸣技能快捷键(默认e)" 65 | 66 | msgid "Auto Combat" 67 | msgstr "自动战斗" 68 | 69 | msgid "Backward" 70 | msgstr "后退" 71 | 72 | msgid "Bell-Borne Geochelone" 73 | msgstr "乌龟" 74 | 75 | msgid "Crownless" 76 | msgstr "无冠者" 77 | 78 | msgid "Enable auto combat in Abyss, Game World etc" 79 | msgstr "在大世界,深渊,无音区等开启自动战斗" 80 | 81 | msgid "Feilian Beringal" 82 | msgstr "猴子" 83 | 84 | msgid "Forward" 85 | msgstr "前进" 86 | 87 | msgid "Impermanence Heron" 88 | msgstr "黑鸟" 89 | 90 | msgid "Inferno Rider" 91 | msgstr "摩托车" 92 | 93 | msgid "Lampylumen Myriad" 94 | msgstr "辉萤军势" 95 | 96 | msgid "Mech Abomination" 97 | msgstr "机器人" 98 | 99 | msgid "Mourning Aix" 100 | msgstr "哀声鸷" 101 | 102 | msgid "N/A" 103 | msgstr "不刷" 104 | 105 | msgid "Skip Dialog during Quests" 106 | msgstr "任务跳过对话" 107 | 108 | msgid "Tempest Mephis" 109 | msgstr "第一个雷Boss" 110 | 111 | msgid "Thundering Mephis" 112 | msgstr "第二个雷Boss" 113 | 114 | msgid "Combat Count" 115 | msgstr "战斗次数" 116 | 117 | msgid "Echo Count" 118 | msgstr "获得声骸个数" 119 | 120 | msgid "Log" 121 | msgstr "日志" 122 | 123 | msgid "Auto Pick" 124 | msgstr "自动拾取" 125 | 126 | msgid "Auto Pick Flowers in Game World" 127 | msgstr "大世界自动拾取" 128 | -------------------------------------------------------------------------------- /src/char/CharFactory.py: -------------------------------------------------------------------------------- 1 | from src.char.Baizhi import Baizhi 2 | from src.char.Calcharo import Calcharo 3 | from src.char.Changli import Changli 4 | from src.char.CharSkillButton import is_float 5 | from src.char.Chixia import Chixia 6 | from src.char.Danjin import Danjin 7 | from src.char.Jinhsi import Jinhsi 8 | from src.char.Yuanwu import Yuanwu 9 | 10 | 11 | def get_char_by_pos(task, box, index): 12 | from src.char.Verina import Verina 13 | from src.char.Yinlin import Yinlin 14 | from src.char.Taoqi import Taoqi 15 | from src.char.BaseChar import BaseChar 16 | from src.char.HavocRover import HavocRover 17 | from src.char.Sanhua import Sanhua 18 | from src.char.Jianxin import Jianxin 19 | from src.char.Encore import Encore 20 | char_dict = { 21 | 'char_yinlin': {'cls': Yinlin, 'res_cd': 12, 'echo_cd': 15}, 22 | 'char_verina': {'cls': Verina, 'res_cd': 12, 'echo_cd': 20}, 23 | 'char_taoqi': {'cls': Taoqi, 'res_cd': 15, 'echo_cd': 20}, 24 | 'char_rover': {'cls': HavocRover, 'res_cd': 12, 'echo_cd': 20}, 25 | 'char_encore': {'cls': Encore, 'res_cd': 10, 'echo_cd': 20}, 26 | 'char_jianxin': {'cls': Jianxin, 'res_cd': 12, 'echo_cd': 20}, 27 | 'char_sanhua': {'cls': Sanhua, 'res_cd': 10, 'echo_cd': 20}, 28 | 'char_jinhsi': {'cls': Jinhsi, 'res_cd': 3, 'echo_cd': 20}, 29 | 'char_yuanwu': {'cls': Yuanwu, 'res_cd': 3, 'echo_cd': 20}, 30 | 'chang_changli': {'cls': Changli, 'res_cd': 12, 'echo_cd': 20}, 31 | 'char_chixia': {'cls': Chixia, 'res_cd': 9, 'echo_cd': 20}, 32 | 'char_danjin': {'cls': Danjin, 'res_cd': 9999999, 'echo_cd': 20}, 33 | 'char_baizhi': {'cls': Baizhi, 'res_cd': 16, 'echo_cd': 20}, 34 | 'char_calcharo': {'cls': Calcharo, 'res_cd': 99999, 'echo_cd': 20} 35 | } 36 | highest_confidence = 0 37 | info = None 38 | for char_name, char_info in char_dict.items(): 39 | feature = task.find_one(char_name, box=box, threshold=0.7) 40 | if feature: 41 | task.log_info(f'found char {char_name} {feature.confidence} {highest_confidence}') 42 | if feature and feature.confidence > highest_confidence: 43 | highest_confidence = feature.confidence 44 | info = char_info 45 | if info is not None: 46 | cls = info.get('cls') 47 | return cls(task, index, info.get('res_cd'), info.get('echo_cd')) 48 | task.log_info(f'could not find char {info} {highest_confidence}') 49 | has_cd = task.ocr(box=box) 50 | if has_cd and is_float(has_cd[0].name): 51 | task.log_info(f'found char {has_cd[0]} wait and reload') 52 | task.next_frame() 53 | return get_char_by_pos(task, box, index) 54 | if task.debug: 55 | task.screenshot(f'could not find char {index}') 56 | return BaseChar(task, index) 57 | -------------------------------------------------------------------------------- /src/char/Encore.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from src.char.BaseChar import BaseChar, Priority 4 | 5 | 6 | class Encore(BaseChar): 7 | 8 | def __init__(self, *args): 9 | super().__init__(*args) 10 | self.last_heavy = 0 11 | self.liberation_time = 0 12 | self.last_resonance = 0 13 | 14 | def still_in_liberation(self): 15 | return time.time() - self.liberation_time < 10 16 | 17 | def do_perform(self): 18 | target_low_con = False 19 | if self.has_intro: 20 | self.sleep(0.7) 21 | self.wait_down() 22 | elif self.can_resonance_step2(4): 23 | if self.click_resonance()[0]: 24 | self.logger.info('try Encore resonance_step2 success') 25 | self.sleep(0.3) 26 | else: 27 | self.task.wait_until(self.resonance_available, time_out=1) 28 | wait_success = self.click_resonance()[0] 29 | self.logger.info(f'try Encore resonance_step2 wait_success:{wait_success}') 30 | 31 | if self.still_in_liberation(): 32 | # if time.time() - self.liberation_time > 7.5 and self.is_forte_full(): 33 | # self.heavy_attack() 34 | # self.last_heavy = time.time() 35 | # else: 36 | target_low_con = True 37 | self.n4() 38 | elif self.click_resonance()[0]: 39 | self.logger.debug('click_resonance') 40 | self.last_resonance = time.time() 41 | pass 42 | elif self.click_liberation(): 43 | self.liberation_time = time.time() 44 | self.n4() 45 | target_low_con = True 46 | elif self.echo_available(): 47 | self.logger.debug('click_echo') 48 | self.click_echo(duration=1.5) 49 | else: 50 | self.logger.info('Encore nothing is available') 51 | self.switch_next_char(target_low_con=target_low_con) 52 | 53 | def count_liberation_priority(self): 54 | return 40 55 | 56 | def count_resonance_priority(self): 57 | return 40 58 | 59 | def count_echo_priority(self): 60 | return 40 61 | 62 | def can_resonance_step2(self, delay=3): 63 | return time.time() - self.last_resonance < delay 64 | 65 | def do_get_switch_priority(self, current_char: BaseChar, has_intro=False): 66 | if time.time() - self.last_heavy < 3: 67 | return Priority.MIN 68 | elif self.still_in_liberation() or self.can_resonance_step2(): 69 | self.logger.info( 70 | f'switch priority MIN because still in liberation') 71 | return Priority.MAX + 1 72 | else: 73 | return super().do_get_switch_priority(current_char, has_intro) 74 | 75 | def n4(self, duration=2.0): 76 | duration = 2.6 if self.click_resonance()[0] else 2.3 77 | if time.time() - self.liberation_time < 6 or not self.is_forte_full(): 78 | self.continues_normal_attack(duration=duration) 79 | if not self.still_in_liberation(): 80 | self.click_resonance() 81 | else: 82 | self.heavy_attack() 83 | self.logger.info('encore heavy') 84 | self.last_heavy = time.time() 85 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ok.util.path import get_path_in_package 4 | from src.task.AutoCombatTask import AutoCombatTask 5 | from src.task.AutoPickTask import AutoPickTask 6 | from src.task.FarmEchoTask import FarmEchoTask 7 | from src.task.FarmWorldBossTask import FarmWorldBossTask 8 | from src.task.SkipDialogTask import AutoDialogTask 9 | 10 | version = "v0.0.11" 11 | 12 | 13 | def calculate_pc_exe_path(running_path): 14 | return running_path 15 | 16 | 17 | config = { 18 | 'debug': False, # Optional, default: False 19 | 'use_gui': True, 20 | 'config_folder': 'configs', 21 | 'gui_icon': get_path_in_package(__file__, 'icon.png'), 22 | 'ocr': { 23 | 'lib': 'RapidOCR' 24 | }, 25 | # required if using feature detection 26 | 'template_matching': { 27 | 'coco_feature_json': os.path.join('assets', '_annotations.coco.json'), 28 | 'default_horizontal_variance': 0.002, 29 | 'default_vertical_variance': 0.002, 30 | 'default_threshold': 0.9, 31 | }, 32 | 'windows': { # required when supporting windows game 33 | 'exe': 'Client-Win64-Shipping.exe', 34 | 'calculate_pc_exe_path': calculate_pc_exe_path, 35 | 'interaction': 'PostMessage', 36 | 'can_bit_blt': True, # default false, opengl games does not support bit_blt 37 | 'bit_blt_render_full': True, 38 | 'check_hdr_and_night_light': True 39 | }, 40 | 'supported_resolution': { 41 | 'ratio': '16:9', 42 | 'min_size': (1600, 900) 43 | }, 44 | 'analytics': { 45 | 'report_url': 'http://111.231.71.225/report' 46 | }, 47 | 'update': { 48 | 'releases_url': 'https://api.github.com/repos/ok-oldking/ok-wuthering-waves/releases?per_page=15', 49 | 'proxy_url': 'http://111.231.71.225/', 50 | 'exe_name': 'ok-ww.exe', 51 | 'use_proxy': True 52 | }, 53 | 'about': """ 54 |
GitHub https://github.com/ok-oldking/ok-wuthering-waves>
56 | 57 |QQ群:970523295
58 |QQ频道:OK-WW
59 |60 | 本软件是免费开源的。 如果你被收费,请立即退款。请访问QQ频道或GitHub下载最新的官方版本。 61 |
62 |63 | 本软件仅供个人使用,用于学习Python编程、计算机视觉、UI自动化等。 请勿将其用于任何营利性或商业用途。 64 |
65 |66 | 使用本软件可能会导致账号被封。 请在了解风险后再使用。 67 |
68 | """, 69 | 'supported_screen_ratio': '16:9', 70 | 'screenshots_folder': "screenshots", 71 | 'gui_title': 'OK-WW', # Optional 72 | # 'coco_feature_folder': get_path(__file__, 'assets/coco_feature'), # required if using feature detection 73 | 'log_file': 'logs/ok-script.log', # Optional, auto rotating every day 74 | 'error_log_file': 'logs/ok-script_error.log', 75 | 'version': version, 76 | 'onetime_tasks': [ # tasks to execute 77 | FarmEchoTask, 78 | FarmWorldBossTask 79 | ], 'trigger_tasks': [ 80 | AutoCombatTask, 81 | AutoDialogTask, 82 | AutoPickTask 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /src/task/SkipDialogTask.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | from ok.feature.FindFeature import FindFeature 5 | from ok.logging.Logger import get_logger 6 | from ok.ocr.OCR import OCR 7 | from ok.task.TriggerTask import TriggerTask 8 | 9 | logger = get_logger(__name__) 10 | 11 | 12 | class AutoDialogTask(TriggerTask, FindFeature, OCR): 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.skip = None 17 | self.confirm_dialog_checked = False 18 | self.trigger_interval = 1 19 | self.has_eye_time = 0 20 | self.name = "Skip Dialog during Quests" 21 | 22 | def run(self): 23 | pass 24 | 25 | def trigger(self): 26 | skip = self.ocr(0.03, 0.03, 0.11, 0.10, use_grayscale=True, match=re.compile('SKIP'), threshold=0.9) 27 | if skip: 28 | logger.info('Click Skip Dialog') 29 | self.click_box(skip, move_back=True) 30 | if not self.confirm_dialog_checked: 31 | logger.info('Start checking if confirm dialog exists') 32 | self.sleep(2) 33 | if self.calculate_color_percentage(dialog_white_color, box=self.box_of_screen(0.42, 0.59, 0.56, 34 | 0.64)) > 0.9 and self.calculate_color_percentage( 35 | dialog_black_color, box=self.box_of_screen(0.61, 0.60, 0.74, 0.64)) > 0.8: 36 | logger.info('confirm dialog exists, click confirm') 37 | self.click_relative(0.44, 0.55) 38 | self.sleep(0.2) 39 | self.click_relative(0.67, 0.62) 40 | else: 41 | self.screenshot('dialog') 42 | logger.info('confirm dialog does not exist') 43 | self.confirm_dialog_checked = True 44 | if time.time() - self.has_eye_time < 2: 45 | btn_dialog_close = self.find_one('btn_dialog_close', use_gray_scale=True, threshold=0.8) 46 | if btn_dialog_close: 47 | self.click(btn_dialog_close, move_back=True) 48 | return 49 | btn_dialog_eye = self.find_one('btn_dialog_eye', use_gray_scale=True, threshold=0.8) 50 | if btn_dialog_eye: 51 | self.has_eye_time = time.time() 52 | btn_auto_play_dialog = self.find_one('btn_auto_play_dialog', use_gray_scale=True) 53 | if btn_auto_play_dialog: 54 | self.click_box(btn_auto_play_dialog, move_back=True) 55 | logger.info('toggle auto play') 56 | self.sleep(0.2) 57 | if arrow := self.find_feature('btn_dialog_arrow', x=0.59, y=0.33, to_x=0.75, to_y=0.75, 58 | use_gray_scale=True, threshold=0.7): 59 | self.click(arrow[-1]) 60 | logger.info('choose arrow') 61 | self.sleep(0.2) 62 | elif dots := self.find_feature('btn_dialog_3dots', x=0.59, y=0.33, to_x=0.75, to_y=0.75, 63 | use_gray_scale=True, threshold=0.7): 64 | if dots: 65 | self.click(dots[-1]) 66 | logger.info('choose dot') 67 | self.sleep(0.2) 68 | return 69 | 70 | 71 | dialog_white_color = { 72 | 'r': (230, 255), # Red range 73 | 'g': (230, 255), # Green range 74 | 'b': (230, 255) # Blue range 75 | } 76 | 77 | dialog_black_color = { 78 | 'r': (0, 15), # Red range 79 | 'g': (0, 15), # Green range 80 | 'b': (0, 15) # Blue range 81 | } 82 | -------------------------------------------------------------------------------- /src/char/CharSkillButton.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import cv2 4 | 5 | from ok.logging.Logger import get_logger 6 | 7 | logger = get_logger(__name__) 8 | 9 | 10 | class CharSkillButton: 11 | 12 | def __init__(self, name, task, t, white_limit=1, white_hints=[]): 13 | self.name = name 14 | self.type = t 15 | if not white_limit: 16 | white_limit = 1 17 | self.white_limit = white_limit 18 | self.white_hints = white_hints 19 | self.task = task 20 | self.white_list = [] 21 | self.white_off_percent = 0.01 22 | 23 | # def is_available(self, percent): 24 | # if percent == 0: 25 | # return True 26 | # for base in self.white_list: 27 | # if abs(base - percent) < self.white_off_percent: 28 | # return True 29 | # if len(self.white_list) == self.white_limit: 30 | # return False 31 | # for white_hint in self.white_hints: 32 | # if abs(percent - white_hint) < self.white_off_percent: 33 | # logger.info(f'{self.name} set base {self.type} to {percent:.4f} by white_hint {white_hint}') 34 | # self.white_list.append(percent) 35 | # return True 36 | # cd_text = self.task.ocr(box=self.task.get_box_by_name(f'box_{self.type}'), target_height=540, threshold=0.9) 37 | # if len(cd_text) == 0 or all(not is_float(text.name) for text in cd_text): 38 | # self.white_list.append(percent) 39 | # if self.task.debug: 40 | # self.task.screenshot(f'{self.name}_{self.type}_{percent:.4f}') 41 | # logger.info( 42 | # f' set base {self.name}_{self.type} to {percent:.4f} by ocr {self.white_list} {self.white_limit} {cd_text}') 43 | # return True 44 | # if cd_text: 45 | # logger.info(f'{self.name} set base {self.type} to has text {cd_text}') 46 | # return False 47 | 48 | def is_available(self, percent): 49 | if percent == 0: 50 | return True 51 | start = time.time() 52 | box = self.task.get_box_by_name(f'box_{self.type}') 53 | box = box.copy(x_offset=box.width / 4, y_offset=box.height * 0.6, width_offset=-box.width / 2, 54 | height_offset=-box.height * 0.5) 55 | dot = self.task.find_one('edge_echo_cd_dot', box=box, canny_lower=40, canny_higher=80, threshold=0.6) 56 | # if self.task.debug: 57 | # colored = cv2.cvtColor(self.boss_lv_edge, cv2.COLOR_GRAY2BGR) 58 | # self.frame[self.boss_lv_box.y:self.boss_lv_box.y + self.boss_lv_box.height, 59 | # self.boss_lv_box.x:self.boss_lv_box.x + self.boss_lv_box.width] = cv2.cvtColor(current, 60 | # cv2.COLOR_GRAY2BGR) 61 | if dot is None: 62 | logger.debug(f'find dot not exist cost : {time.time() - start}') 63 | return True 64 | else: 65 | logger.debug(f'find dot exist cost : {time.time() - start} {dot}') 66 | return False 67 | 68 | 69 | def is_float(s): 70 | try: 71 | float(s) 72 | return True 73 | except ValueError: 74 | return False 75 | 76 | 77 | if __name__ == '__main__': 78 | image = cv2.imread( 79 | 'assets\\images\\154eb284-17_01_16_406889_WindowsGraphicsCaptureMethod_3840x2160_title_None_Clie_6yGu616.png', 80 | cv2.IMREAD_GRAYSCALE) # Load in grayscale 81 | 82 | # Apply Canny edge detection 83 | edges = cv2.Canny(image, 40, 80) 84 | 85 | # Save the result 86 | cv2.imwrite('edges.jpg', edges) 87 | -------------------------------------------------------------------------------- /main_debug.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from pathlib import Path 3 | import rapidocr_openvino 4 | 5 | 6 | block_cipher = None 7 | 8 | package_name = 'rapidocr_openvino' 9 | install_dir = Path(rapidocr_openvino.__file__).resolve().parent 10 | 11 | onnx_paths = list(install_dir.rglob('*.onnx')) + list(install_dir.rglob('*.txt')) 12 | yaml_paths = list(install_dir.rglob('*.yaml')) 13 | 14 | onnx_add_data = [(str(v.parent), f'{package_name}/{v.parent.name}') 15 | for v in onnx_paths] 16 | 17 | yaml_add_data = [] 18 | for v in yaml_paths: 19 | if package_name == v.parent.name: 20 | yaml_add_data.append((str(v.parent / '*.yaml'), package_name)) 21 | else: 22 | yaml_add_data.append( 23 | (str(v.parent / '*.yaml'), f'{package_name}/{v.parent.name}')) 24 | 25 | import openvino 26 | 27 | block_cipher = None 28 | 29 | package_name = 'openvino' 30 | install_dir = Path(openvino.__file__).resolve().parent 31 | 32 | openvino_dll_path = list(install_dir.rglob('openvino_intel_cpu_plugin.dll')) + list(install_dir.rglob('openvino_onnx_frontend.dll')) 33 | 34 | 35 | # Modified list comprehension with a condition check 36 | openvino_add_data = [(str(v), f'{package_name}/{v.parent.name}') 37 | for v in openvino_dll_path] 38 | 39 | print(f'openvino_add_data {openvino_add_data}') 40 | add_data = list(set(yaml_add_data + onnx_add_data + openvino_add_data)) 41 | 42 | excludes = ['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter', 'resources', 'matplotlib','numpy.lib'] 43 | add_data.append(('icon.ico', '.')) 44 | 45 | def list_files(directory, prefix=''): 46 | file_list = [] 47 | for root, dirs, files in os.walk(directory): 48 | for filename in files: 49 | # Create the full filepath by joining root with the filename 50 | filepath = os.path.join(root, filename) 51 | # Create the relative path for the file to be used in the spec datas 52 | relative_path = os.path.relpath(filepath, prefix) 53 | folder_path = os.path.dirname(relative_path) 54 | # Append the tuple (full filepath, relative path) to the file list 55 | file_list.append((filepath, folder_path)) 56 | return file_list 57 | 58 | if os.path.exists('assets'): 59 | root_folder = os.getcwd() # Get the current working directory 60 | assets = list_files(os.path.join(root_folder, 'assets'), root_folder) 61 | add_data += assets 62 | 63 | print(f"add_data {add_data}") 64 | 65 | a = Analysis( 66 | ['main_debug.py'], 67 | pathex=[], 68 | binaries=[], 69 | datas=add_data, 70 | hiddenimports=[], 71 | hookspath=[], 72 | hooksconfig={}, 73 | runtime_hooks=[], 74 | excludes=[], 75 | cipher=block_cipher, 76 | noarchive=False, 77 | noconsole=True, 78 | ) 79 | 80 | 81 | # List of patterns to exclude 82 | exclude_patterns = ['opencv_videoio_ffmpeg', 'opengl32sw.dll', 'Qt6Quick.dll','Qt6Pdf.dll','Qt6Qml.dll','Qt6OpenGL.dll','Qt6Network.dll','Qt6QmlModels.dll','Qt6VirtualKeyboard.dll','QtNetwork.pyd' 83 | ,'openvino_pytorch_frontend.dll','openvino_tensorflow_frontend.dll','py_tensorflow_frontend.cp311-win_amd64.pyd','py_pytorch_frontend.cp311-win_amd64.pyd', 84 | ] 85 | 86 | 87 | # Optimized list comprehension using any() with a generator expression 88 | a.binaries = [x for x in a.binaries if not any(pattern in x[0] for pattern in exclude_patterns)] 89 | 90 | print(f'a.binaries {a.binaries}') 91 | 92 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 93 | 94 | exe = EXE( 95 | pyz, 96 | a.scripts, 97 | [], 98 | exclude_binaries=True, 99 | name='ok-baijing', 100 | icon='icon.ico', 101 | debug=False, 102 | bootloader_ignore_signals=False, 103 | strip=False, 104 | upx=True, 105 | console=True, 106 | disable_windowed_traceback=False, 107 | argv_emulation=False, 108 | target_arch=None, 109 | codesign_identity=None, 110 | entitlements_file=None, 111 | ) 112 | 113 | coll = COLLECT( 114 | exe, 115 | a.binaries, 116 | a.datas, 117 | strip=False, 118 | upx=True, 119 | upx_exclude=[], 120 | name='bundle', 121 | ) 122 | 123 | -------------------------------------------------------------------------------- /src/char/Jinhsi.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from src.char.BaseChar import BaseChar, Priority 4 | 5 | 6 | class Jinhsi(BaseChar): 7 | 8 | def __init__(self, *args): 9 | super().__init__(*args) 10 | self.last_free_intro = 0 # free intro every 25 sec 11 | self.has_free_intro = False 12 | self.incarnation = False 13 | self.incarnation_cd = False 14 | 15 | def do_perform(self): 16 | if self.incarnation: 17 | self.handle_incarnation() 18 | return self.switch_next_char() 19 | if self.has_intro or self.incarnation_cd: 20 | self.handle_intro() 21 | return self.switch_next_char() 22 | self.click_echo() 23 | return self.switch_next_char() 24 | 25 | def reset_state(self): 26 | super().reset_state() 27 | self.incarnation = False 28 | self.has_free_intro = False 29 | self.incarnation_cd = False 30 | 31 | def switch_next_char(self, **args): 32 | super().switch_next_char(free_intro=self.has_free_intro, target_low_con=True) 33 | self.has_free_intro = False 34 | 35 | def do_get_switch_priority(self, current_char: BaseChar, has_intro=False): 36 | if has_intro or self.incarnation or self.incarnation_cd: 37 | self.logger.info( 38 | f'switch priority max because has_intro {has_intro} incarnation {self.incarnation} incarnation_cd {self.incarnation_cd}') 39 | return Priority.MAX 40 | else: 41 | return super().do_get_switch_priority(current_char, has_intro) 42 | 43 | def count_base_priority(self): 44 | return 0 45 | 46 | def count_resonance_priority(self): 47 | return 0 48 | 49 | def count_echo_priority(self): 50 | return 0 51 | 52 | def count_liberation_priority(self): 53 | return 0 54 | 55 | def handle_incarnation(self): 56 | self.incarnation = False 57 | self.logger.info(f'handle_incarnation click_resonance start') 58 | start = time.time() 59 | liberated = False 60 | while True: 61 | current_res = self.current_resonance() 62 | if current_res > 0 and not self.has_cd('resonance'): 63 | self.logger.debug(f'handle_incarnation current_res: {current_res} breaking') 64 | # if self.task.debug: 65 | # self.task.screenshot(f'handle_incarnation e available') 66 | break 67 | self.task.click(interval=0.1) 68 | if not liberated or not self.task.in_team()[0]: 69 | self.check_combat() 70 | 71 | self.click_resonance(has_animation=True, send_click=True) 72 | if not self.click_echo(): 73 | self.task.click() 74 | # if self.task.debug: 75 | # self.task.screenshot(f'handle_incarnation click_resonance end {time.time() - start}') 76 | self.logger.info(f'handle_incarnation click_resonance end {time.time() - start}') 77 | 78 | def handle_intro(self): 79 | # self.task.screenshot(f'handle_intro start') 80 | self.logger.info(f'handle_intro start') 81 | last = None 82 | start = time.time() 83 | self.send_resonance_key() 84 | while not self.has_cd('resonance'): 85 | if last != 'resonance' or time.time() - start < 1: 86 | if self.send_resonance_key(interval=0.1): 87 | last = 'resonance' 88 | else: 89 | if self.task.click(interval=0.1): 90 | last = 'click' 91 | self.check_combat() 92 | if time.time() - start < 1.2: 93 | self.logger.info(f'handle_intro fly e in_cd {time.time() - start}') 94 | self.incarnation_cd = True 95 | if not self.click_echo(): 96 | self.task.click() 97 | return 98 | if self.click_liberation(send_click=True): 99 | self.continues_normal_attack(0.3) 100 | else: 101 | self.continues_normal_attack(1.8) 102 | # self.task.screenshot(f'handle_intro end {time.time() - start}') 103 | self.logger.info(f'handle_intro end {time.time() - start}') 104 | self.incarnation = True 105 | self.incarnation_cd = False 106 | 107 | def wait_resonance(self): 108 | while not self.resonance_available(check_ready=True): 109 | self.send_resonance_key(interval=0.1) 110 | -------------------------------------------------------------------------------- /src/task/FarmEchoTask.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ok.logging.Logger import get_logger 4 | from src.task.BaseCombatTask import BaseCombatTask 5 | 6 | logger = get_logger(__name__) 7 | 8 | 9 | class FarmEchoTask(BaseCombatTask): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.description = "Click Start at the Entrance(Dreamless, Jue)" 14 | self.name = "Farm Echo in Dungeon" 15 | self.default_config.update({ 16 | 'Level': 1, 17 | 'Repeat Farm Count': 100, 18 | 'Entrance Direction': 'Forward' 19 | }) 20 | self.config_description = { 21 | 'Level': '(1-6) Important, Choose which level to farm, lower levels might not produce a echo', 22 | 'Entrance Direction': 'Choose Forward for Dreamless, Backward for Jue' 23 | } 24 | self.config_type["Entrance Direction"] = {'type': "drop_down", 'options': ['Forward', 'Backward']} 25 | self.crownless_pos = (0.9, 0.4) 26 | self.last_drop = False 27 | 28 | def run(self): 29 | # return self.run_in_circle_to_find_echo() 30 | self.handler.post(self.mouse_reset, 0.01) 31 | if not self.in_team()[0]: 32 | self.log_error('must be in game world and in teams', notify=True) 33 | return 34 | 35 | # loop here 36 | count = 0 37 | 38 | while count < self.config.get("Repeat Farm Count", 0): 39 | count += 1 40 | self.wait_in_team_and_world(time_out=20) 41 | self.sleep(1) 42 | self.walk_until_f(time_out=10, 43 | direction='w' if self.config.get('Entrance Direction') == 'Forward' else 's', 44 | raise_if_not_found=True) 45 | logger.info(f'enter success') 46 | stam = self.wait_ocr(0.75, 0.02, 0.85, 0.09, match=re.compile('240'), raise_if_not_found=True) 47 | logger.info(f'found stam {stam}') 48 | self.sleep(1) 49 | self.choose_level(self.config.get("Level")) 50 | 51 | self.combat_once() 52 | logger.info(f'farm echo combat end') 53 | self.wait_in_team_and_world(time_out=20) 54 | logger.info(f'farm echo move {self.config.get("Entrance Direction")} walk_until_f to find echo') 55 | if self.config.get('Entrance Direction') == 'Forward': 56 | dropped = self.walk_until_f(time_out=3, 57 | raise_if_not_found=False) # find and pick echo 58 | logger.debug(f'farm echo found echo move forward walk_until_f to find echo') 59 | else: 60 | self.sleep(2) 61 | dropped = self.run_in_circle_to_find_echo(3) 62 | self.incr_drop(dropped) 63 | self.sleep(0.5) 64 | self.send_key('esc') 65 | self.wait_click_feature('gray_confirm_exit_button', relative_x=-1, raise_if_not_found=True, 66 | use_gray_scale=True) 67 | self.wait_in_team_and_world(time_out=40) 68 | self.sleep(4) 69 | if self.config.get('Entrance Direction') == 'Backward': 70 | self.send_key('a', down_time=0.2) # Jue 71 | self.sleep(1) 72 | 73 | def incr_drop(self, dropped): 74 | if dropped: 75 | self.info['Echo Count'] = self.info.get('Echo Count', 0) + 1 76 | self.last_drop = dropped 77 | 78 | def choose_level(self, start): 79 | y = 0.17 80 | x = 0.15 81 | distance = 0.08 82 | 83 | logger.info(f'choose level {start}') 84 | self.click_relative(x, y + (start - 1) * distance) 85 | self.sleep(0.5) 86 | 87 | self.wait_click_feature('gray_button_challenge', raise_if_not_found=True, use_gray_scale=True, 88 | click_after_delay=0.5) 89 | self.wait_click_feature('gray_confirm_exit_button', relative_x=-1, raise_if_not_found=False, 90 | use_gray_scale=True, time_out=3, click_after_delay=0.5, threshold=0.8) 91 | self.wait_click_feature('gray_start_battle', relative_x=-1, raise_if_not_found=True, 92 | use_gray_scale=True, click_after_delay=0.5, threshold=0.8) 93 | 94 | 95 | echo_color = { 96 | 'r': (200, 255), # Red range 97 | 'g': (150, 220), # Green range 98 | 'b': (130, 170) # Blue range 99 | } 100 | -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: pythzon ; coding: utf-8 -*- 2 | from pathlib import Path 3 | import importlib.util 4 | block_cipher = None 5 | 6 | def check_package_exists(package_name): 7 | package_spec = importlib.util.find_spec(package_name) 8 | return package_spec is not None 9 | 10 | # Example usage: 11 | if check_package_exists('rapidocr_openvino'): 12 | print("rapidocr_openvino exists") 13 | 14 | import rapidocr_openvino 15 | 16 | 17 | 18 | 19 | package_name = 'rapidocr_openvino' 20 | install_dir = Path(rapidocr_openvino.__file__).resolve().parent 21 | 22 | onnx_paths = list(install_dir.rglob('*.onnx')) + list(install_dir.rglob('*.txt')) 23 | yaml_paths = list(install_dir.rglob('*.yaml')) 24 | 25 | onnx_add_data = [(str(v.parent), f'{package_name}/{v.parent.name}') 26 | for v in onnx_paths] 27 | 28 | yaml_add_data = [] 29 | for v in yaml_paths: 30 | if package_name == v.parent.name: 31 | yaml_add_data.append((str(v.parent / '*.yaml'), package_name)) 32 | else: 33 | yaml_add_data.append( 34 | (str(v.parent / '*.yaml'), f'{package_name}/{v.parent.name}')) 35 | 36 | import openvino 37 | 38 | 39 | package_name = 'openvino' 40 | install_dir = Path(openvino.__file__).resolve().parent 41 | 42 | openvino_dll_path = list(install_dir.rglob('openvino_intel_cpu_plugin.dll')) + list(install_dir.rglob('openvino_onnx_frontend.dll')) 43 | 44 | 45 | # Modified list comprehension with a condition check 46 | openvino_add_data = [(str(v), f'{package_name}/{v.parent.name}') 47 | for v in openvino_dll_path] 48 | 49 | print(f'openvino_add_data {openvino_add_data}') 50 | add_data = list(set(yaml_add_data + onnx_add_data + openvino_add_data)) 51 | else: 52 | add_data = [] 53 | 54 | excludes = ['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter', 'resources', 'matplotlib','numpy.lib'] 55 | add_data.append(('icon.png', '.')) 56 | 57 | 58 | def list_files(directory, prefix='', extensions=[]): 59 | file_list = [] 60 | for root, dirs, files in os.walk(directory): 61 | for filename in files: 62 | # Check the file extension 63 | _, ext = os.path.splitext(filename) 64 | if extensions and ext not in extensions: 65 | continue 66 | 67 | # Create the full filepath by joining root with the filename 68 | filepath = os.path.join(root, filename) 69 | 70 | # Create the relative path for the file to be used in the spec datas 71 | relative_path = os.path.relpath(filepath, prefix) 72 | folder_path = os.path.dirname(relative_path) 73 | 74 | # Append the tuple (full filepath, relative path) to the file list 75 | file_list.append((filepath, folder_path)) 76 | return file_list 77 | 78 | if os.path.exists('assets'): 79 | root_folder = os.getcwd() # Get the current working directory 80 | assets = list_files(os.path.join(root_folder, 'assets'), root_folder) 81 | add_data += assets 82 | 83 | if os.path.exists('i18n'): 84 | root_folder = os.getcwd() # Get the current working directory 85 | assets = list_files(os.path.join(root_folder, 'i18n'), root_folder, ['.mo']) 86 | add_data += assets 87 | 88 | print(f"add_data {add_data}") 89 | 90 | a = Analysis( 91 | ['main.py'], 92 | pathex=[], 93 | binaries=[], 94 | datas=add_data, 95 | hiddenimports=[], 96 | hookspath=[], 97 | hooksconfig={}, 98 | runtime_hooks=[], 99 | excludes=[], 100 | cipher=block_cipher, 101 | noarchive=False, 102 | noconsole=True, 103 | ) 104 | 105 | 106 | # List of patterns to exclude 107 | exclude_patterns = ['opencv_videoio_ffmpeg', 'opengl32sw.dll', 'Qt6Quick.dll','Qt6Pdf.dll','Qt6Qml.dll','Qt6OpenGL.dll','Qt6Network.dll','Qt6QmlModels.dll','Qt6VirtualKeyboard.dll','QtNetwork.pyd' 108 | ,'openvino_pytorch_frontend.dll','openvino_tensorflow_frontend.dll','py_tensorflow_frontend.cp311-win_amd64.pyd','py_pytorch_frontend.cp311-win_amd64.pyd', 109 | ] 110 | 111 | 112 | # Optimized list comprehension using any() with a generator expression 113 | a.binaries = [x for x in a.binaries if not any(pattern in x[0] for pattern in exclude_patterns)] 114 | 115 | print(f'a.binaries {a.binaries}') 116 | 117 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 118 | 119 | from config import config 120 | 121 | exe = EXE( 122 | pyz, 123 | a.scripts, 124 | [], 125 | exclude_binaries=True, 126 | name='ok-ww', 127 | icon='icon.png', 128 | debug=False, 129 | bootloader_ignore_signals=False, 130 | strip=False, 131 | upx=True, 132 | console=False, 133 | disable_windowed_traceback=False, 134 | argv_emulation=False, 135 | target_arch=None, 136 | codesign_identity=None, 137 | entitlements_file=None, 138 | uac_admin=True, 139 | ) 140 | 141 | coll = COLLECT( 142 | exe, 143 | a.binaries, 144 | a.datas, 145 | strip=False, 146 | upx=True, 147 | upx_exclude=[], 148 | name='bundle', 149 | ) 150 | 151 | -------------------------------------------------------------------------------- /src/task/FarmWorldBossTask.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from ok.feature.Feature import Feature 4 | from ok.logging.Logger import get_logger 5 | from src.task.BaseCombatTask import BaseCombatTask, CharDeadException 6 | 7 | logger = get_logger(__name__) 8 | 9 | 10 | class FarmWorldBossTask(BaseCombatTask): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.description = "Click Start in Game World" 15 | self.name = "Farm World Boss(Must Drop a WayPoint on the Boss First)" 16 | self.boss_names = ['N/A', 'Crownless', 'Tempest Mephis', 'Thundering Mephis', 'Inferno Rider', 17 | 'Feilian Beringal', 18 | 'Mourning Aix', 'Impermanence Heron', 'Lampylumen Myriad', 'Mech Abomination', 19 | 'Bell-Borne Geochelone'] 20 | self.weekly_boss_index = {'Bell-Borne Geochelone': 3} 21 | self.weekly_boss_count = 1 # Bell-Borne Geochelone 22 | default_config = { 23 | 'Boss1': 'N/A', 24 | 'Boss2': 'N/A', 25 | 'Boss3': 'N/A', 26 | 'Repeat Farm Count': 1000 27 | } 28 | default_config.update(self.default_config) 29 | self.default_config = default_config 30 | self.config_type["Boss1"] = {'type': "drop_down", 'options': self.boss_names} 31 | self.config_type["Boss2"] = {'type': "drop_down", 'options': self.boss_names} 32 | self.config_type["Boss3"] = {'type': "drop_down", 'options': self.boss_names} 33 | self.config_description = { 34 | 'Level': '(1-6) Important, Choose which level to farm, lower levels might not produce a echo', 35 | 'Entrance Direction': 'Choose Forward for Dreamless, Backward for Jue' 36 | } 37 | self.config_type["Entrance Direction"] = {'type': "drop_down", 'options': ['Forward', 'Backward']} 38 | self.crownless_pos = (0.9, 0.4) 39 | self.last_drop = False 40 | 41 | def teleport_to_boss(self, boss_name): 42 | index = self.boss_names.index(boss_name) 43 | index -= 1 44 | self.log_info(f'teleport to {boss_name} index {index}') 45 | self.sleep(1) 46 | self.log_info('click f2 to open the book') 47 | self.send_key('f2') 48 | gray_book_boss = self.wait_book() 49 | if not gray_book_boss: 50 | self.log_error("can't find the gray_book_boss", notify=True) 51 | raise Exception("can't find gray_book_boss") 52 | 53 | self.log_info(f'click {gray_book_boss}') 54 | self.click_box(gray_book_boss) 55 | self.sleep(1.5) 56 | 57 | if index >= (len(self.boss_names) - self.weekly_boss_count - 1): # weekly turtle 58 | logger.info('click weekly boss') 59 | index = self.weekly_boss_index[boss_name] 60 | self.click_relative(0.21, 0.59) 61 | else: 62 | logger.info('click normal boss') 63 | self.click_relative(0.21, 0.36) 64 | 65 | self.sleep(1) 66 | 67 | if index > 4: 68 | self.log_info(f'click scroll bar') 69 | self.click_relative(3760 / 3840, 1852 / 2160) 70 | self.sleep(0.5) 71 | index -= 4 72 | 73 | self.log_info(f'index after scrolling down {index}') 74 | proceeds = self.find_feature('boss_proceed', vertical_variance=1, use_gray_scale=True, threshold=0.8) 75 | if self.debug: 76 | self.screenshot('proceeds') 77 | if not proceeds: 78 | raise Exception("can't find the boss proceeds") 79 | 80 | self.wait_feature('gray_teleport', raise_if_not_found=True, use_gray_scale=True, time_out=120, 81 | pre_action=lambda: self.click_box(proceeds[index], relative_x=-1), wait_until_before_delay=5) 82 | self.sleep(1) 83 | teleport = self.wait_click_feature('custom_teleport', box=self.box_of_screen(0.48, 0.45, 0.54, 0.58), 84 | raise_if_not_found=False, threshold=0.8, time_out=2) 85 | if not teleport: 86 | self.click_relative(0.5, 0.5) 87 | self.sleep(0.5) 88 | self.wait_click_feature('gray_custom_way_point', box=self.box_of_screen(0.62, 0.48, 0.70, 0.66), 89 | raise_if_not_found=True, 90 | use_gray_scale=True, threshold=0.75, time_out=2) 91 | self.click_fast_travel() 92 | 93 | def click_fast_travel(self): 94 | travel = self.wait_feature('fast_travel_custom', raise_if_not_found=True, threshold=0.75) 95 | self.click_box(travel, relative_x=1.5) 96 | 97 | def wait_book(self): 98 | gray_book_boss = self.wait_until( 99 | lambda: self.find_one('gray_book_boss', vertical_variance=1, horizontal_variance=0.05, 100 | threshold=0.7, canny_lower=50, 101 | canny_higher=150) or self.find_one( 102 | 'gray_book_boss_highlight', 103 | vertical_variance=1, horizontal_variance=0.05, 104 | threshold=0.7, 105 | canny_lower=50, 106 | canny_higher=150), 107 | time_out=3) 108 | return gray_book_boss 109 | 110 | def check_main(self): 111 | if not self.in_team()[0]: 112 | self.send_key('esc') 113 | self.sleep(1) 114 | return self.in_team()[0] 115 | return True 116 | 117 | # not current in use because not stable, right now using one click to scroll down 118 | def scroll_down_a_page(self): 119 | source_box = self.box_of_screen(0.38, 0.80, 0.42, 0.83) 120 | source_template = Feature(source_box.crop_frame(self.frame), source_box.x, source_box.y) 121 | target_box = self.box_of_screen(0.38, 0.16, 0.42, 0.31) 122 | start = time.time() 123 | 124 | self.click_relative(0.5, 0.5) 125 | self.sleep(0.1) 126 | while True: 127 | if time.time() - start > 20: 128 | raise Exception("scroll to long") 129 | self.scroll_relative(0.5, 0.5, -1) 130 | self.sleep(0.1) 131 | targets = self.find_feature('target_box', box=target_box, template=source_template) 132 | if targets: 133 | self.log_info(f'scroll to targets {targets} successfully') 134 | break 135 | 136 | def teleport_to_heal(self): 137 | self.info['Death Count'] = self.info.get('Death Count', 0) + 1 138 | self.send_key('esc') 139 | self.sleep(1) 140 | self.log_info('click m to open the map') 141 | self.send_key('m') 142 | self.sleep(2) 143 | self.click_relative(0.68, 0.05) 144 | self.sleep(1) 145 | self.click_relative(0.37, 0.42) 146 | travel = self.wait_feature('gray_teleport', raise_if_not_found=True, use_gray_scale=True, time_out=3) 147 | self.click_box(travel, relative_x=1.5) 148 | self.wait_in_team_and_world(time_out=20) 149 | self.sleep(2) 150 | 151 | def run(self): 152 | if not self.check_main(): 153 | self.log_error('must be in game world and in teams', notify=True) 154 | self.handler.post(self.mouse_reset, 0.01) 155 | count = 0 156 | while True: 157 | for i in range(1, 4): 158 | key = 'Boss' + str(i) 159 | if boss_name := self.config.get(key): 160 | if boss_name != 'N/A': 161 | count += 1 162 | self.teleport_to_boss(boss_name) 163 | logger.info(f'farm echo combat once start') 164 | if boss_name == 'Crownless': 165 | self.wait_in_team_and_world(time_out=20) 166 | self.sleep(2) 167 | logger.info('Crownless walk to f') 168 | self.walk_until_f(raise_if_not_found=True, time_out=4, backward_time=1) 169 | try: 170 | self.combat_once() 171 | except CharDeadException: 172 | logger.info(f'char dead try teleport to heal') 173 | self.teleport_to_heal() 174 | continue 175 | logger.info(f'farm echo combat end') 176 | if boss_name == 'Bell-Borne Geochelone': 177 | logger.info(f'sleep for the Boss model to disappear') 178 | self.sleep(5) 179 | self.wait_in_team_and_world(time_out=20) 180 | logger.info(f'farm echo move forward walk_until_f to find echo') 181 | if self.walk_until_f(time_out=6, backward_time=1, 182 | raise_if_not_found=False): # find and pick echo 183 | logger.debug(f'farm echo found echo move forward walk_until_f to find echo') 184 | self.incr_drop(True) 185 | 186 | if count == 0: 187 | self.log_error('must choose at least 1 Boss to Farm', notify=True) 188 | return 189 | 190 | def incr_drop(self, dropped): 191 | if dropped: 192 | self.info['Echo Count'] = self.info.get('Echo Count', 0) + 1 193 | self.last_drop = dropped 194 | -------------------------------------------------------------------------------- /src/combat/CombatCheck.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | import cv2 5 | 6 | from ok.color.Color import find_color_rectangles, get_mask_in_color_range, is_pure_black 7 | from ok.feature.Box import find_boxes_by_name 8 | from ok.logging.Logger import get_logger 9 | from src import text_white_color 10 | 11 | logger = get_logger(__name__) 12 | 13 | 14 | class CombatCheck: 15 | 16 | def __init__(self): 17 | self._in_combat = False 18 | self.boss_lv_template = None 19 | self.boss_lv_mask = None 20 | self.in_liberation = False # return True 21 | self.has_count_down = False 22 | self.last_out_of_combat_time = 0 23 | self.last_combat_check = 0 24 | self.boss_lv_box = None 25 | self.boss_health_box = None 26 | self.boss_health = None 27 | self.out_of_combat_reason = "" 28 | self.combat_check_interval = 0.8 29 | self.last_click_liberation = 0 30 | 31 | def reset_to_false(self, recheck=False, reason=""): 32 | if is_pure_black(self.frame): 33 | logger.error('getting a pure black frame for unknown reason, reset_to_false return true') 34 | return True 35 | if recheck and time.time() - self.last_out_of_combat_time > 2.1: 36 | logger.info('out of combat start double check') 37 | if self.debug: 38 | self.screenshot('out of combat start double check') 39 | self.last_out_of_combat_time = time.time() 40 | return True 41 | else: 42 | self.out_of_combat_reason = reason 43 | self._in_combat = False 44 | self.boss_lv_mask = None 45 | self.boss_lv_template = None 46 | self.in_liberation = False # return True 47 | self.has_count_down = False 48 | self.last_out_of_combat_time = 0 49 | self.last_combat_check = 0 50 | self.boss_lv_box = None 51 | self.boss_health = None 52 | self.boss_health_box = None 53 | return False 54 | 55 | def recent_liberation(self): 56 | return time.time() - self.last_click_liberation < 3 57 | 58 | def check_count_down(self): 59 | count_down_area = self.box_of_screen(1820 / 3840, 266 / 2160, 2100 / 3840, 60 | 340 / 2160, name="check_count_down") 61 | count_down = self.calculate_color_percentage(text_white_color, 62 | count_down_area) 63 | 64 | if self.has_count_down: 65 | if count_down < 0.03: 66 | numbers = self.ocr(box=count_down_area, match=count_down_re) 67 | if self.debug: 68 | self.screenshot(f'count_down disappeared {count_down:.2f}%') 69 | logger.info(f'count_down disappeared {numbers} {count_down:.2f}%') 70 | if not numbers: 71 | self.has_count_down = False 72 | return False 73 | else: 74 | return True 75 | else: 76 | return True 77 | else: 78 | if count_down > 0.03: 79 | numbers = self.ocr(box=count_down_area, match=count_down_re) 80 | if numbers: 81 | self.has_count_down = True 82 | logger.info(f'set count_down to {self.has_count_down} {numbers} {count_down:.2f}%') 83 | return self.has_count_down 84 | 85 | def check_boss(self, in_team): 86 | current = self.boss_lv_box.crop_frame(self.frame) 87 | max_val = 0 88 | if current is not None: 89 | res = cv2.matchTemplate(current, self.boss_lv_template, cv2.TM_CCOEFF_NORMED, mask=self.boss_lv_mask) 90 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 91 | if max_val < 0.8: 92 | if self.debug: 93 | self.screenshot_boss_lv(current, f'boss lv not detected by edge {max_val}') 94 | logger.debug(f'boss lv not detected by edge') 95 | if not self.find_boss_lv_text(): # double check by text 96 | if not in_team and not self.check_health_bar() and not self.check_count_down() and not self.find_target_enemy(): 97 | if self.debug: 98 | self.screenshot_boss_lv(current, 'out_of combat boss_health disappeared') 99 | logger.info(f'out of combat because of boss_health disappeared, res:{max_val}') 100 | return False 101 | else: 102 | self.boss_lv_template = None 103 | self.boss_lv_box = None 104 | logger.info(f'boss_health disappeared, but still in combat') 105 | return True 106 | else: 107 | return True 108 | else: 109 | logger.debug(f'check boss edge passed {max_val}') 110 | return True 111 | 112 | def screenshot_boss_lv(self, current, name): 113 | if self.debug: 114 | if self.boss_lv_box is not None and self.boss_lv_template is not None and current is not None: 115 | frame = self.frame.copy() 116 | frame[self.boss_lv_box.y:self.boss_lv_box.y + self.boss_lv_box.height, 117 | self.boss_lv_box.x:self.boss_lv_box.x + self.boss_lv_box.width] = current 118 | x, y, w, h = self.boss_lv_box.x, self.boss_lv_box.height + 50 + self.boss_lv_box.y, self.boss_lv_box.width, self.boss_lv_box.height 119 | frame[y:y + h, x:x + w] = self.boss_lv_template 120 | self.screenshot(name, frame) 121 | 122 | def find_target_enemy(self): 123 | start = time.time() 124 | target_enemy = self.find_one('target_enemy_white', box=self.box_of_screen(0.25, 0.25, 0.75, 0.75), 125 | use_gray_scale=True, threshold=0.83, 126 | frame_processor=process_target_enemy_area) 127 | # if self.debug and target_enemy is not None: 128 | # self.screenshot('find_target_enemy') 129 | logger.debug(f'find_target_enemy {target_enemy} {time.time() - start}') 130 | return target_enemy is not None 131 | 132 | def handle_monthly_card(self): 133 | monthly_card = self.find_one('monthly_card', threshold=0.8) 134 | if monthly_card is not None: 135 | self.click(monthly_card) 136 | self.sleep(2) 137 | self.click(monthly_card) 138 | self.sleep(1) 139 | logger.debug(f'check_monthly_card {monthly_card}') 140 | return monthly_card is not None 141 | 142 | def in_combat(self, rechecked=False): 143 | if self.in_liberation or self.recent_liberation(): 144 | self.last_combat_check = time.time() 145 | logger.debug('in liberation return True') 146 | return True 147 | if self._in_combat: 148 | now = time.time() 149 | if now - self.last_combat_check > self.combat_check_interval: 150 | self.last_combat_check = now 151 | in_team = self.in_team()[0] 152 | if not in_team: 153 | return self.reset_to_false(recheck=False, reason="not in team") 154 | if self.check_count_down(): 155 | return True 156 | if self.boss_lv_template is not None: 157 | if self.check_boss(in_team): 158 | return True 159 | else: 160 | return self.reset_to_false(recheck=False, reason="boss disappear") 161 | if not self.check_health_bar(): 162 | logger.debug('not in team or no health bar') 163 | if not self.target_enemy(): 164 | logger.error('target_enemy failed, break out of combat') 165 | return self.reset_to_false(reason='target enemy failed') 166 | return True 167 | else: 168 | logger.debug( 169 | 'check in combat pass') 170 | return True 171 | else: 172 | return True 173 | else: 174 | in_combat = self.in_team()[0] and self.check_health_bar() 175 | if in_combat: 176 | in_combat = self.boss_health_box is not None or self.boss_lv_template is not None or self.has_count_down 177 | if in_combat: 178 | self.target_enemy(wait=False) 179 | else: 180 | in_combat = self.target_enemy() 181 | if in_combat: 182 | logger.info( 183 | f'enter combat boss_lv_template:{self.boss_lv_template is not None} boss_health_box:{self.boss_health_box} has_count_down:{self.has_count_down}') 184 | self._in_combat = True 185 | return True 186 | 187 | def target_enemy(self, wait=True): 188 | if not wait: 189 | self.middle_click() 190 | else: 191 | if self.find_target_enemy(): 192 | return True 193 | self.middle_click() 194 | return self.wait_until(self.find_target_enemy, time_out=2) 195 | 196 | def check_health_bar(self): 197 | if self._in_combat: 198 | min_height = self.height_of_screen(10 / 2160) 199 | max_height = min_height * 3 200 | min_width = self.width_of_screen(15 / 3840) 201 | else: 202 | min_height = self.height_of_screen(12 / 2160) 203 | max_height = min_height * 3 204 | min_width = self.width_of_screen(100 / 3840) 205 | 206 | boxes = find_color_rectangles(self.frame, enemy_health_color_red, min_width, min_height, max_height=max_height) 207 | 208 | if len(boxes) > 0: 209 | self.draw_boxes('enemy_health_bar_red', boxes, color='blue') 210 | return True 211 | else: 212 | boxes = find_color_rectangles(self.frame, boss_health_color, min_width * 3, min_height * 1.3, 213 | box=self.box_of_screen(1269 / 3840, 58 / 2160, 2533 / 3840, 192 / 2160)) 214 | if len(boxes) == 1: 215 | self.boss_health_box = boxes[0] 216 | self.boss_health_box.width = 10 217 | self.boss_health_box.x += 6 218 | self.boss_health = self.boss_health_box.crop_frame(self.frame) 219 | self.draw_boxes('boss_health', boxes, color='blue') 220 | return True 221 | 222 | return self.find_boss_lv_text() 223 | 224 | def find_boss_lv_text(self): 225 | texts = self.ocr(box=self.box_of_screen(1269 / 3840, 10 / 2160, 2533 / 3840, 140 / 2160), 226 | target_height=720) 227 | boss_lv_texts = find_boxes_by_name(texts, 228 | [re.compile(r'(?i)^L[V].*')]) 229 | if len(boss_lv_texts) > 0: 230 | logger.debug(f'boss_lv_texts: {boss_lv_texts}') 231 | self.boss_lv_box = boss_lv_texts[0] 232 | self.boss_lv_template, self.boss_lv_mask = self.keep_boss_text_white() 233 | if self.boss_lv_template is None: 234 | self.boss_lv_box = None 235 | return False 236 | return True 237 | 238 | def keep_boss_text_white(self): 239 | cropped = self.boss_lv_box.crop_frame(self.frame) 240 | mask, area = get_mask_in_color_range(cropped, boss_white_text_color) 241 | if area / mask.shape[0] * mask.shape[1] < 0.05: 242 | mask, area = get_mask_in_color_range(cropped, boss_orange_text_color) 243 | if area / mask.shape[0] * mask.shape[1] < 0.05: 244 | mask, area = get_mask_in_color_range(cropped, 245 | boss_red_text_color) 246 | if area / mask.shape[0] * mask.shape[1] < 0.05: 247 | logger.error(f'keep_boss_text_white cant find text with the correct color') 248 | return None, 0 249 | return cropped, mask 250 | 251 | 252 | count_down_re = re.compile(r'\d\d') 253 | 254 | 255 | def process_target_enemy_area(frame): 256 | frame[frame != 255] = 0 257 | return frame 258 | 259 | 260 | enemy_health_color_red = { 261 | 'r': (202, 212), # Red range 262 | 'g': (70, 80), # Green range 263 | 'b': (55, 65) # Blue range 264 | } # 207,75,60 265 | 266 | enemy_health_color_black = { 267 | 'r': (10, 55), # Red range 268 | 'g': (28, 50), # Green range 269 | 'b': (18, 70) # Blue range 270 | } 271 | 272 | boss_white_text_color = { 273 | 'r': (200, 255), # Red range 274 | 'g': (200, 255), # Green range 275 | 'b': (200, 255) # Blue range 276 | } 277 | 278 | boss_orange_text_color = { 279 | 'r': (218, 218), # Red range 280 | 'g': (178, 178), # Green range 281 | 'b': (68, 68) # Blue range 282 | } 283 | 284 | boss_red_text_color = { 285 | 'r': (200, 230), # Red range 286 | 'g': (70, 90), # Green range 287 | 'b': (60, 80) # Blue range 288 | } 289 | 290 | boss_health_color = { 291 | 'r': (250, 255), # Red range 292 | 'g': (30, 180), # Green range 293 | 'b': (4, 75) # Blue range 294 | } 295 | -------------------------------------------------------------------------------- /src/task/BaseCombatTask.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | import win32api 5 | 6 | from ok.config.ConfigOption import ConfigOption 7 | from ok.feature.FindFeature import FindFeature 8 | from ok.logging.Logger import get_logger 9 | from ok.ocr.OCR import OCR 10 | from ok.task.BaseTask import BaseTask 11 | from ok.task.TaskExecutor import CannotFindException 12 | from ok.util.list import safe_get 13 | from src.char import BaseChar 14 | from src.char.BaseChar import Priority 15 | from src.char.CharFactory import get_char_by_pos 16 | from src.combat.CombatCheck import CombatCheck 17 | 18 | logger = get_logger(__name__) 19 | 20 | 21 | class NotInCombatException(Exception): 22 | pass 23 | 24 | 25 | class CharDeadException(NotInCombatException): 26 | pass 27 | 28 | 29 | key_config_option = ConfigOption('Game Hotkey Config', { 30 | 'Echo Key': 'q', 31 | 'Liberation Key': 'r', 32 | 'Resonance Key': 'e', 33 | }, description='In Game Hotkey for Skills') 34 | 35 | 36 | class BaseCombatTask(BaseTask, FindFeature, OCR, CombatCheck): 37 | 38 | def __init__(self): 39 | super().__init__() 40 | CombatCheck.__init__(self) 41 | self.chars = [None, None, None] 42 | self.char_texts = ['char_1_text', 'char_2_text', 'char_3_text'] 43 | self.key_config = self.get_config(key_config_option) 44 | 45 | self.mouse_pos = None 46 | 47 | self.char_texts = ['char_1_text', 'char_2_text', 'char_3_text'] 48 | 49 | def raise_not_in_combat(self, message, exception_type=None): 50 | logger.error(message) 51 | self.reset_to_false(reason=message) 52 | if exception_type is None: 53 | exception_type = NotInCombatException 54 | raise exception_type(message) 55 | 56 | def combat_once(self, wait_combat_time=180, wait_before=2): 57 | self.wait_until(lambda: self.in_combat(), time_out=wait_combat_time, raise_if_not_found=True) 58 | self.sleep(wait_before) 59 | self.wait_until(lambda: self.in_combat(), time_out=3, raise_if_not_found=True) 60 | self.load_chars() 61 | self.info['Combat Count'] = self.info.get('Combat Count', 0) + 1 62 | while self.in_combat(): 63 | try: 64 | logger.debug(f'combat_once loop {self.chars}') 65 | self.get_current_char().perform() 66 | except CharDeadException as e: 67 | raise e 68 | except NotInCombatException as e: 69 | logger.info(f'combat_once out of combat break {e}') 70 | if self.debug: 71 | self.screenshot(f'out of combat break {self.out_of_combat_reason}') 72 | break 73 | 74 | def run_in_circle_to_find_echo(self, circle_count=3): 75 | directions = ['w', 'a', 's', 'd'] 76 | step = 1.2 77 | duration = 0.8 78 | total_index = 0 79 | for count in range(circle_count): 80 | logger.debug(f'running first circle_count{circle_count} circle {total_index} duration:{duration}') 81 | for direction in directions: 82 | if total_index > 2 and (total_index + 1) % 2 == 0: 83 | duration += step 84 | picked = self.send_key_and_wait_f(direction, False, time_out=duration, running=True) 85 | if picked: 86 | self.mouse_up(key="right") 87 | return True 88 | total_index += 1 89 | 90 | def switch_next_char(self, current_char, post_action=None, free_intro=False, target_low_con=False): 91 | max_priority = Priority.MIN 92 | switch_to = None 93 | has_intro = free_intro 94 | if not has_intro: 95 | current_con = current_char.get_current_con() 96 | if current_con > 0.8 and current_con != 1: 97 | logger.info(f'switch_next_char current_con {current_con:.2f} almost full, sleep and check again') 98 | self.sleep(0.05) 99 | self.next_frame() 100 | current_con = current_char.get_current_con() 101 | if current_con == 1: 102 | has_intro = True 103 | 104 | for i, char in enumerate(self.chars): 105 | if char == current_char: 106 | priority = Priority.CURRENT_CHAR 107 | else: 108 | priority = char.get_switch_priority(current_char, has_intro) 109 | if target_low_con: 110 | priority += (1 - char.current_con) * 1000 - Priority.SWITCH_CD 111 | logger.info( 112 | f'switch_next_char priority: {char} {priority} {char.current_con} target_low_con {target_low_con}') 113 | if priority > max_priority: 114 | max_priority = priority 115 | switch_to = char 116 | if switch_to == current_char: 117 | self.check_combat() 118 | self.click() 119 | logger.warning(f"can't find next char to switch to, maybe switching too fast click and wait") 120 | return self.switch_next_char(current_char, post_action, free_intro, target_low_con) 121 | switch_to.has_intro = has_intro 122 | logger.info(f'switch_next_char {current_char} -> {switch_to} has_intro {has_intro}') 123 | last_click = 0 124 | start = time.time() 125 | while True: 126 | now = time.time() 127 | if now - last_click > 0.1: 128 | self.send_key(switch_to.index + 1) 129 | last_click = now 130 | in_team, current_index, size = self.in_team() 131 | if not in_team: 132 | if self.debug: 133 | self.screenshot(f'not in team while switching chars_{current_char}_to_{switch_to} {now - start}') 134 | confirm = self.wait_feature('revive_confirm', threshold=0.8, time_out=3) 135 | if confirm: 136 | self.log_info(f'char dead') 137 | self.raise_not_in_combat(f'char dead', exception_type=CharDeadException) 138 | else: 139 | self.raise_not_in_combat( 140 | f'not in team while switching chars_{current_char}_to_{switch_to}') 141 | if now - start > 10: 142 | self.raise_not_in_combat( 143 | f'switch too long failed chars_{current_char}_to_{switch_to}, {now - start}') 144 | if current_index != switch_to.index: 145 | has_intro = free_intro if free_intro else current_char.is_con_full() 146 | switch_to.has_intro = has_intro 147 | if now - start > 10: 148 | if self.debug: 149 | self.screenshot(f'switch_not_detected_{current_char}_to_{switch_to}') 150 | self.raise_not_in_combat('failed switch chars') 151 | else: 152 | self.next_frame() 153 | else: 154 | self.in_liberation = False 155 | switch_time = time.time() 156 | current_char.switch_out() 157 | switch_to.is_current_char = True 158 | break 159 | 160 | if post_action: 161 | post_action() 162 | logger.info(f'switch_next_char end {(switch_time - start):.3f}s') 163 | return switch_time 164 | 165 | def click(self, x=-1, y=-1, move_back=False, name=None, interval=-1): 166 | if x == -1 and y == -1: 167 | x = self.width_of_screen(0.5) 168 | y = self.height_of_screen(0.5) 169 | return super().click(x, y, move_back, name, interval) 170 | 171 | def wait_in_team_and_world(self, time_out=10, raise_if_not_found=True): 172 | return self.wait_until(self.in_team_and_world, time_out=time_out, raise_if_not_found=raise_if_not_found) 173 | 174 | def in_team_and_world(self): 175 | return self.in_team()[ 176 | 0] # and self.find_one(f'gray_book_button', threshold=0.7, canny_lower=50, canny_higher=150) 177 | 178 | def get_current_char(self): 179 | for char in self.chars: 180 | if char.is_current_char: 181 | return char 182 | if not self.in_team()[0]: 183 | self.raise_not_in_combat('can find current char!!') 184 | self.load_chars() 185 | return self.get_current_char() 186 | 187 | def sleep_check_combat(self, timeout, check_combat=True): 188 | start = time.time() 189 | if not self.in_combat() and check_combat: 190 | self.raise_not_in_combat('sleep check not in combat') 191 | self.sleep(timeout - (time.time() - start)) 192 | 193 | def check_combat(self): 194 | if not self.in_combat(): 195 | if self.debug: 196 | self.screenshot('not_in_combat_calling_check_combat') 197 | self.raise_not_in_combat('combat check not in combat') 198 | 199 | def send_key_and_wait_f(self, direction, raise_if_not_found, time_out, running=False): 200 | if time_out <= 0: 201 | return 202 | start = time.time() 203 | if running: 204 | self.mouse_down(key='right') 205 | self.send_key_down(direction) 206 | f_found = self.wait_feature('pick_up_f', horizontal_variance=0.01, vertical_variance=0.01, threshold=0.8, 207 | wait_until_before_delay=0, time_out=time_out, raise_if_not_found=False) 208 | if f_found: 209 | self.send_key('f') 210 | self.sleep(0.1) 211 | self.send_key_up(direction) 212 | if running: 213 | self.mouse_up(key='right') 214 | if not f_found: 215 | if raise_if_not_found: 216 | raise CannotFindException('cant find the f to enter') 217 | else: 218 | logger.warning(f"can't find the f to enter") 219 | return False 220 | remaining = time.time() - start 221 | 222 | if self.handle_claim_button(): 223 | self.sleep(0.5) 224 | self.send_key_down(direction) 225 | if running: 226 | self.mouse_down(key='right') 227 | self.sleep(remaining + 0.2) 228 | if running: 229 | self.mouse_up(key='right') 230 | self.send_key_up(direction) 231 | return False 232 | return f_found 233 | 234 | def handle_claim_button(self): 235 | if self.wait_feature('cancel_button', raise_if_not_found=False, horizontal_variance=0.1, 236 | vertical_variance=0.1, 237 | use_gray_scale=True, time_out=2, threshold=0.8): 238 | self.send_key('esc') 239 | self.sleep(0.05) 240 | logger.info(f"found a claim reward") 241 | return True 242 | 243 | def walk_until_f(self, direction='w', time_out=0, raise_if_not_found=True, backward_time=0): 244 | if not self.find_one('pick_up_f', horizontal_variance=0.01, vertical_variance=0.01, threshold=0.8): 245 | if backward_time > 0: 246 | if self.send_key_and_wait_f('s', raise_if_not_found, backward_time): 247 | return True 248 | return self.send_key_and_wait_f(direction, raise_if_not_found, time_out) and self.sleep(0.5) 249 | else: 250 | self.send_key('f') 251 | if self.handle_claim_button(): 252 | return False 253 | self.sleep(0.5) 254 | return True 255 | 256 | def load_chars(self): 257 | in_team, current_index, count = self.in_team() 258 | if not in_team: 259 | return 260 | self.log_info('load chars') 261 | char = get_char_by_pos(self, self.get_box_by_name('box_char_1'), 0) 262 | old_char = safe_get(self.chars, 0) 263 | if self.should_update(char, old_char): 264 | self.chars[0] = char 265 | logger.info(f'update char1 to {char.name} {type(char)} {type(char) is not BaseChar}') 266 | 267 | char = get_char_by_pos(self, self.get_box_by_name('box_char_2'), 1) 268 | old_char = safe_get(self.chars, 1) 269 | if self.should_update(char, old_char): 270 | self.chars[1] = char 271 | logger.info(f'update char2 to {char.name}') 272 | if count == 3: 273 | char = get_char_by_pos(self, self.get_box_by_name('box_char_3'), 2) 274 | old_char = safe_get(self.chars, 2) 275 | if self.should_update(char, old_char): 276 | if len(self.chars) == 3: 277 | self.chars[2] = char 278 | else: 279 | self.chars.append(char) 280 | logger.info(f'update char3 to {char.name}') 281 | else: 282 | if len(self.chars) == 3: 283 | self.chars.pop(0) 284 | logger.info(f'team size changed to 2') 285 | 286 | for char in self.chars: 287 | char.reset_state() 288 | if char.index == current_index: 289 | char.is_current_char = True 290 | else: 291 | char.is_current_char = False 292 | 293 | self.log_info(f'load chars success {self.chars}') 294 | 295 | @staticmethod 296 | def should_update(char, old_char): 297 | return (type(char) is BaseChar and old_char is None) or (type(char) is not BaseChar and old_char != char) 298 | 299 | def box_resonance(self): 300 | return self.get_box_by_name('box_resonance_cd') 301 | 302 | def get_resonance_cd_percentage(self): 303 | return self.calculate_color_percentage(white_color, self.get_box_by_name('box_resonance_cd')) 304 | 305 | def get_resonance_percentage(self): 306 | return self.calculate_color_percentage(white_color, self.get_box_by_name('box_resonance')) 307 | 308 | def in_team(self): 309 | start = time.time() 310 | c1 = self.find_one('char_1_text', 311 | threshold=0.75) 312 | c2 = self.find_one('char_2_text', 313 | threshold=0.75) 314 | c3 = self.find_one('char_3_text', 315 | threshold=0.75) 316 | arr = [c1, c2, c3] 317 | # logger.debug(f'in_team check {arr} time: {(time.time() - start):.3f}s') 318 | current = -1 319 | exist_count = 0 320 | for i in range(len(arr)): 321 | if arr[i] is None: 322 | if current == -1: 323 | current = i 324 | else: 325 | exist_count += 1 326 | if exist_count == 2 or exist_count == 1: 327 | return True, current, exist_count + 1 328 | else: 329 | return False, -1, exist_count + 1 330 | 331 | def mouse_reset(self): 332 | # logger.debug("mouse_reset") 333 | try: 334 | current_position = win32api.GetCursorPos() 335 | if self.mouse_pos: 336 | distance = math.sqrt( 337 | (current_position[0] - self.mouse_pos[0]) ** 2 338 | + (current_position[1] - self.mouse_pos[1]) ** 2 339 | ) 340 | if distance > 400: 341 | logger.debug(f'move mouse back {self.mouse_pos}') 342 | win32api.SetCursorPos(self.mouse_pos) 343 | self.mouse_pos = None 344 | if self.enabled: 345 | self.handler.post(self.mouse_reset, 1) 346 | return 347 | self.mouse_pos = current_position 348 | if self.enabled: 349 | return self.handler.post(self.mouse_reset, 0.005) 350 | except Exception as e: 351 | logger.error('mouse_reset exception', e) 352 | 353 | 354 | white_color = { 355 | 'r': (253, 255), # Red range 356 | 'g': (253, 255), # Green range 357 | 'b': (253, 255) # Blue range 358 | } 359 | -------------------------------------------------------------------------------- /src/char/BaseChar.py: -------------------------------------------------------------------------------- 1 | import time 2 | from enum import IntEnum, StrEnum 3 | from typing import Any 4 | 5 | import cv2 6 | import numpy as np 7 | 8 | from ok.color.Color import get_connected_area_by_color, color_range_to_bound 9 | from ok.config.Config import Config 10 | from ok.logging.Logger import get_logger 11 | from src import text_white_color 12 | 13 | 14 | class Priority(IntEnum): 15 | MIN = -999999999 16 | SWITCH_CD = -1000 17 | CURRENT_CHAR = -100 18 | SKILL_AVAILABLE = 100 19 | ALL_IN_CD = 0 20 | NORMAL = 10 21 | MAX = 9999999999 22 | 23 | 24 | class Role(StrEnum): 25 | DEFAULT = 'Default' 26 | SUB_DPS = 'Sub DPS' 27 | MAIN_DPS = 'Main DPS' 28 | HEALER = 'Healer' 29 | 30 | 31 | role_values = [role for role in Role] 32 | 33 | char_lib_check_marks = ['char_1_lib_check_mark', 'char_2_lib_check_mark', 'char_3_lib_check_mark'] 34 | 35 | 36 | class BaseChar: 37 | 38 | def __init__(self, task, index, res_cd=0, echo_cd=0): 39 | self.white_off_threshold = 0.01 40 | self.echo_cd = echo_cd 41 | self.task = task 42 | self.sleep_adjust = 0 43 | self.index = index 44 | self.last_switch_time = -1 45 | self.last_res = -1 46 | self.last_echo = -1 47 | self.has_intro = False 48 | self.res_cd = res_cd 49 | self.is_current_char = False 50 | self.liberation_available_mark = False 51 | self.logger = get_logger(self.name) 52 | self.full_ring_area = 0 53 | self._is_forte_full = False 54 | self.config = {"_full_ring_area": 0, "_ring_color_index": -1} 55 | if type(self) is not BaseChar: 56 | self.config = Config(self.name, self.config) 57 | self.current_con = 0 58 | 59 | def char_config(self): 60 | return {} 61 | 62 | @property 63 | def name(self): 64 | return self.__class__.__name__ 65 | 66 | def __eq__(self, other): 67 | if isinstance(other, BaseChar): 68 | return self.name == other.name and self.index == other.index 69 | return False 70 | 71 | def perform(self): 72 | # self.wait_down() 73 | self.do_perform() 74 | self.logger.debug(f'set current char false {self.index}') 75 | 76 | def wait_down(self): 77 | start = time.time() 78 | while self.flying(): 79 | self.task.click() 80 | self.sleep(0.2) 81 | 82 | self.task.screenshot( 83 | f'{self}_down_finish_{(time.time() - start):.2f}_f:{self.is_forte_full()}_e:{self.resonance_available()}_r:{self.echo_available()}_q:{self.liberation_available()}_i{self.has_intro}') 84 | 85 | def click(self, *args: Any, **kwargs: Any): 86 | self.task.click(*args, **kwargs) 87 | 88 | def do_perform(self): 89 | self.click_liberation(con_less_than=1) 90 | if self.click_resonance()[0]: 91 | return self.switch_next_char() 92 | if self.click_echo(): 93 | return self.switch_next_char() 94 | self.task.click() 95 | self.switch_next_char() 96 | 97 | def has_cd(self, box_name): 98 | box = self.task.get_box_by_name(f'box_{box_name}') 99 | cropped = box.crop_frame(self.task.frame) 100 | num_labels, stats = get_connected_area_by_color(cropped, dot_color, connectivity=8) 101 | big_area_count = 0 102 | has_dot = False 103 | number_count = 0 104 | invalid_count = 0 105 | for i in range(1, num_labels): 106 | # Check if the connected co mponent touches the border 107 | left, top, width, height, area = stats[i] 108 | if area / self.task.frame.shape[0] / self.task.frame.shape[ 109 | 1] > 20 / 3840 / 2160: 110 | big_area_count += 1 111 | if left > 0 and top > 0 and left + width < box.width and top + height < box.height: 112 | # self.logger.debug(f"{box_name} Area of connected component {i}: {area} pixels {width}x{height}") 113 | if 16 / 3840 / 2160 <= area / self.task.frame.shape[0] / self.task.frame.shape[ 114 | 1] <= 60 / 3840 / 2160 and abs(width - height) / (width + height) < 0.3: 115 | has_dot = True 116 | elif 25 / 2160 <= height / self.task.screen_height <= 45 / 2160 and 5 / 2160 <= width / self.task.screen_height <= 35 / 2160: 117 | number_count += 1 118 | else: 119 | invalid_count += 1 120 | has_cd = invalid_count == 0 and (has_dot and 2 <= number_count <= 3) 121 | # if self.task.debug: 122 | # msg = f"{self}_{has_cd}_{box_name} number_count {number_count} big_count {big_area_count} invalid_count {invalid_count} has_dot {has_dot}" 123 | # self.task.screenshot(msg, frame=cropped) 124 | # self.logger.debug(msg) 125 | return has_cd 126 | 127 | def is_available(self, percent, box_name): 128 | return percent == 0 or not self.has_cd(box_name) 129 | 130 | def switch_out(self): 131 | self.is_current_char = False 132 | self.has_intro = False 133 | self.liberation_available_mark = self.liberation_available() 134 | if self.current_con == 1: 135 | self.logger.info(f'switch_out at full con set current_con to 0') 136 | self.current_con = 0 137 | 138 | def __repr__(self): 139 | return self.__class__.__name__ + ('_T' if self.is_current_char else '_F') 140 | 141 | def switch_next_char(self, post_action=None, free_intro=False, target_low_con=False): 142 | self.is_forte_full() 143 | self.last_switch_time = self.task.switch_next_char(self, post_action=post_action, free_intro=free_intro, 144 | target_low_con=target_low_con) 145 | 146 | def sleep(self, sec, check_combat=True): 147 | if sec > 0: 148 | self.task.sleep_check_combat(sec + self.sleep_adjust, check_combat=check_combat) 149 | 150 | def click_resonance(self, post_sleep=0, has_animation=False, send_click=True): 151 | clicked = False 152 | self.logger.debug(f'click_resonance start') 153 | last_click = 0 154 | last_op = 'click' 155 | resonance_click_time = 0 156 | animated = False 157 | while True: 158 | if resonance_click_time != 0 and time.time() - resonance_click_time > 8: 159 | self.logger.error(f'click_resonance too long, breaking {time.time() - resonance_click_time}') 160 | self.task.screenshot('click_resonance too long, breaking') 161 | break 162 | if has_animation: 163 | if not self.task.in_team()[0]: 164 | self.task.in_liberation = True 165 | animated = True 166 | if time.time() - resonance_click_time > 6: 167 | self.task.in_liberation = False 168 | self.logger.error(f'resonance animation too long, breaking') 169 | self.task.next_frame() 170 | self.check_combat() 171 | continue 172 | self.check_combat() 173 | current_resonance = self.current_resonance() 174 | if not self.resonance_available(current_resonance): 175 | self.logger.debug(f'click_resonance not available break') 176 | break 177 | self.logger.debug(f'click_resonance resonance_available click') 178 | now = time.time() 179 | if now - last_click > 0.1: 180 | if ((current_resonance == 0) and send_click) or last_op == 'resonance': 181 | self.task.click() 182 | last_op = 'click' 183 | continue 184 | if current_resonance > 0: 185 | if resonance_click_time == 0: 186 | clicked = True 187 | resonance_click_time = now 188 | self.update_res_cd() 189 | last_op = 'resonance' 190 | self.send_resonance_key() 191 | if has_animation: # sleep if there will be an animation like Jinhsi 192 | self.sleep(0.2, check_combat=False) 193 | last_click = now 194 | self.task.next_frame() 195 | self.task.in_liberation = False 196 | if clicked: 197 | self.sleep(post_sleep) 198 | duration = time.time() - resonance_click_time if resonance_click_time != 0 else 0 199 | self.logger.info(f'click_resonance end clicked {clicked} duration {duration} animated {animated}') 200 | return clicked, duration, animated 201 | 202 | def send_resonance_key(self, post_sleep=0, interval=-1, down_time=0.01): 203 | self.task.send_key(self.task.key_config.get('Resonance Key'), interval=interval, down_time=down_time) 204 | self.sleep(post_sleep) 205 | 206 | def update_res_cd(self): 207 | current = time.time() 208 | if current - self.last_res > self.res_cd: # count the first click only 209 | self.last_res = time.time() 210 | 211 | def update_echo_cd(self): 212 | current = time.time() 213 | if current - self.last_echo > self.echo_cd: # count the first click only 214 | self.last_echo = time.time() 215 | 216 | def click_echo(self, duration=0, sleep_time=0): 217 | self.logger.debug(f'click_echo start') 218 | clicked = False 219 | start = 0 220 | last_click = 0 221 | while True: 222 | self.check_combat() 223 | current = self.current_echo() 224 | if duration == 0 and not self.echo_available(current): 225 | break 226 | now = time.time() 227 | if duration > 0 and start != 0: 228 | if now - start > duration: 229 | break 230 | self.logger.debug(f'click_echo echo_available click') 231 | if now - last_click > 0.1: 232 | if current == 0: 233 | self.task.click() 234 | else: 235 | if start == 0: 236 | start = now 237 | clicked = True 238 | self.update_echo_cd() 239 | self.task.send_key(self.get_echo_key()) 240 | last_click = now 241 | self.task.next_frame() 242 | self.logger.debug(f'click_echo end {clicked}') 243 | return clicked 244 | 245 | def check_combat(self): 246 | self.task.check_combat() 247 | 248 | def reset_state(self): 249 | self.logger.info('reset state') 250 | self.has_intro = False 251 | 252 | def click_liberation(self, wait_end=True, con_less_than=-1, send_click=False): 253 | if con_less_than > 0: 254 | if self.get_current_con() > con_less_than: 255 | return False 256 | self.task.in_liberation = True 257 | self.logger.debug(f'click_liberation start') 258 | start = time.time() 259 | last_click = 0 260 | clicked = False 261 | while self.liberation_available(): 262 | self.logger.debug(f'click_liberation liberation_available click') 263 | now = time.time() 264 | if now - last_click > 0.1: 265 | self.task.last_click_liberation = now 266 | self.task.send_key(self.get_liberation_key()) 267 | self.liberation_available_mark = False 268 | clicked = True 269 | last_click = now 270 | if time.time() - start > 5: 271 | self.task.raise_not_in_combat('too long clicking a liberation') 272 | self.task.next_frame() 273 | if clicked: 274 | if self.task.wait_until(lambda: not self.task.in_team()[0], time_out=0.6): 275 | self.logger.debug(f'not in_team successfully casted liberation') 276 | else: 277 | self.task.in_liberation = False 278 | self.logger.error(f'clicked liberation but no effect') 279 | return False 280 | while not self.task.in_team()[0]: 281 | clicked = True 282 | if send_click: 283 | self.task.click(interval=0.1) 284 | if time.time() - start > 7: 285 | self.task.raise_not_in_combat('too long a liberation, the boss was killed by the liberation') 286 | self.task.next_frame() 287 | self.task.in_liberation = False 288 | if clicked: 289 | liberation_time = f'{(time.time() - start):.2f}' 290 | self.logger.info(f'click_liberation end {liberation_time}') 291 | return clicked 292 | 293 | def get_liberation_key(self): 294 | return self.task.key_config['Liberation Key'] 295 | 296 | def get_echo_key(self): 297 | return self.task.key_config['Echo Key'] 298 | 299 | def get_resonance_key(self): 300 | return self.task.key_config['Resonance Key'] 301 | 302 | def get_switch_priority(self, current_char, has_intro): 303 | priority = self.do_get_switch_priority(current_char, has_intro) 304 | if priority != Priority.MAX and time.time() - self.last_switch_time < 0.9: 305 | return Priority.SWITCH_CD # switch cd 306 | else: 307 | return priority 308 | 309 | def do_get_switch_priority(self, current_char, has_intro=False): 310 | priority = 0 311 | if self.count_liberation_priority() and self.liberation_available(): 312 | priority += self.count_liberation_priority() 313 | if self.count_resonance_priority() and self.resonance_available(): 314 | priority += self.count_resonance_priority() 315 | if self.count_forte_priority() and self._is_forte_full: 316 | priority += self.count_forte_priority() 317 | if priority > 0: 318 | priority += Priority.SKILL_AVAILABLE 319 | priority += self.count_liberation_priority() 320 | return priority 321 | 322 | def count_base_priority(self): 323 | return 0 324 | 325 | def count_liberation_priority(self): 326 | return 1 327 | 328 | def count_resonance_priority(self): 329 | return 10 330 | 331 | def count_echo_priority(self): 332 | return 1 333 | 334 | def count_forte_priority(self): 335 | return 0 336 | 337 | def resonance_available(self, current=None, check_ready=False): 338 | if self.is_current_char: 339 | snap = self.current_resonance() if current is None else current 340 | if check_ready and snap == 0: 341 | return False 342 | return self.is_available(snap, 'resonance') 343 | elif self.res_cd > 0: 344 | return time.time() - self.last_res > self.res_cd 345 | 346 | def echo_available(self, current=None): 347 | if self.is_current_char: 348 | snap = self.current_echo() if current is None else current 349 | return self.is_available(snap, 'echo') 350 | elif self.echo_cd > 0: 351 | return time.time() - self.last_echo > self.echo_cd 352 | 353 | def is_con_full(self): 354 | return self.get_current_con() == 1 355 | 356 | def get_current_con(self): 357 | box = self.task.box_of_screen(1422 / 3840, 1939 / 2160, 1566 / 3840, 2076 / 2160, name='con_full') 358 | box.confidence = 0 359 | 360 | max_area = 0 361 | percent = 0 362 | max_is_full = False 363 | color_index = -1 364 | target_index = self.config.get('_ring_color_index', -1) 365 | cropped = box.crop_frame(self.task.frame) 366 | for i in range(len(con_colors)): 367 | if target_index != -1 and i != target_index: 368 | continue 369 | color_range = con_colors[i] 370 | area, is_full = self.count_rings(cropped, color_range, 371 | 1500 / 3840 / 2160 * self.task.screen_width * self.task.screen_height) 372 | self.logger.debug(f'is_con_full test color_range {color_range} {area, is_full}') 373 | if is_full: 374 | max_is_full = is_full 375 | color_index = i 376 | if area > max_area: 377 | max_area = int(area) 378 | if max_is_full: 379 | self.logger.info( 380 | f'is_con_full found a full ring {self.config.get("_full_ring_area", 0)} -> {max_area} {color_index}') 381 | self.config['_full_ring_area'] = max_area 382 | self.config['_ring_color_index'] = color_index 383 | self.logger.info( 384 | f'is_con_full2 found a full ring {self.config.get("_full_ring_area", 0)} -> {max_area} {color_index}') 385 | if self.config.get('_full_ring_area', 0) > 0: 386 | percent = max_area / self.config['_full_ring_area'] 387 | if not max_is_full and percent >= 1: 388 | self.logger.warning( 389 | f'is_con_full not full but percent greater than 1, set to 0.99, {percent} {max_is_full}') 390 | # self.task.screenshot( 391 | # f'is_con_full not full but percent greater than 1, set to 0.99, {percent} {max_is_full}', 392 | # cropped) 393 | percent = 0.99 394 | if percent > 1: 395 | self.logger.error(f'is_con_full percent greater than 1, set to 1, {percent} {max_is_full}') 396 | self.task.screenshot(f'is_con_full percent greater than 1, set to 1, {percent} {max_is_full}', cropped) 397 | percent = 1 398 | self.logger.info( 399 | f'is_con_full {self} {percent} {max_area}/{self.config.get("_full_ring_area", 0)} {color_index} ') 400 | # if self.task.debug: 401 | # self.task.screenshot( 402 | # f'is_con_full {self} {percent} {max_area}/{self.config.get("_full_ring_area", 0)} {color_index} ', 403 | # cropped) 404 | box.confidence = percent 405 | self.current_con = percent 406 | self.task.draw_boxes(f'is_con_full_{self}', box) 407 | if percent > 1: 408 | percent = 1 409 | return percent 410 | 411 | def is_forte_full(self): 412 | box = self.task.box_of_screen(2251 / 3840, 1993 / 2160, 2311 / 3840, 2016 / 2160, name='forte_full') 413 | white_percent = self.task.calculate_color_percentage(forte_white_color, box) 414 | # num_labels, stats = get_connected_area_by_color(box.crop_frame(self.task.frame), forte_white_color, 415 | # connectivity=8) 416 | # total_area = 0 417 | # for i in range(1, num_labels): 418 | # # Check if the connected co mponent touches the border 419 | # left, top, width, height, area = stats[i] 420 | # total_area += area 421 | # white_percent = total_area / box.width / box.height 422 | # if self.task.debug: 423 | # self.task.screenshot(f'{self}_forte_{white_percent}') 424 | self.logger.debug(f'is_forte_full {white_percent}') 425 | box.confidence = white_percent 426 | self.task.draw_boxes('forte_full', box) 427 | self._is_forte_full = white_percent > 0.08 428 | return self._is_forte_full 429 | 430 | def liberation_available(self): 431 | if self.liberation_available_mark: 432 | return True 433 | if self.is_current_char: 434 | snap = self.current_liberation() 435 | if snap == 0: 436 | return False 437 | else: 438 | return self.is_available(snap, 'liberation') 439 | # else: 440 | # mark_to_check = char_lib_check_marks[self.index] 441 | # box = self.task.get_box_by_name(mark_to_check) 442 | # box = box.copy(x_offset=-box.width, y_offset=-box.height, width_offset=box.width * 2, 443 | # height_offset=box.height * 2) 444 | # for match in char_lib_check_marks: 445 | # mark = self.task.find_one(match, box=box, canny_lower=10, canny_higher=80, threshold=0.8) 446 | # if mark is not None: 447 | # self.logger.debug(f'{self.__repr__()} liberation ready by checking mark {mark}') 448 | # self.liberation_available_mark = True 449 | # return True 450 | 451 | def __str__(self): 452 | return self.__repr__() 453 | 454 | def continues_normal_attack(self, duration, interval=0.1, click_resonance_if_ready_and_return=False, 455 | until_con_full=False): 456 | start = time.time() 457 | while time.time() - start < duration: 458 | if click_resonance_if_ready_and_return and self.resonance_available(): 459 | return self.click_resonance() 460 | if until_con_full and self.is_con_full(): 461 | return 462 | self.task.click(interval=interval) 463 | 464 | def normal_attack(self): 465 | self.logger.debug('normal attack') 466 | self.check_combat() 467 | self.task.click() 468 | 469 | def heavy_attack(self, duration=0.6): 470 | self.check_combat() 471 | self.logger.debug('heavy attack start') 472 | self.task.mouse_down() 473 | self.sleep(duration) 474 | self.task.mouse_up() 475 | self.logger.debug('heavy attack end') 476 | 477 | def current_resonance(self): 478 | return self.task.calculate_color_percentage(text_white_color, 479 | self.task.get_box_by_name('box_resonance')) 480 | 481 | def current_echo(self): 482 | return self.task.calculate_color_percentage(text_white_color, 483 | self.task.get_box_by_name('box_echo')) 484 | 485 | def current_liberation(self): 486 | return self.task.calculate_color_percentage(text_white_color, self.task.get_box_by_name('box_liberation')) 487 | 488 | def flying(self): 489 | return self.current_resonance() == 0 490 | 491 | def count_rings(self, image, color_range, min_area): 492 | # Define the color range 493 | lower_bound, upper_bound = color_range_to_bound(color_range) 494 | 495 | image_with_contours = image.copy() 496 | 497 | # Create a binary mask 498 | mask = cv2.inRange(image, lower_bound, upper_bound) 499 | 500 | # Find connected components 501 | num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) 502 | 503 | colors = [ 504 | (0, 255, 0), # Green 505 | (0, 0, 255), # Red 506 | (255, 0, 0), # Blue 507 | (0, 255, 255), # Yellow 508 | (255, 0, 255), # Magenta 509 | (255, 255, 0) # Cyan 510 | ] 511 | 512 | # Function to check if a component forms a ring 513 | def is_full_ring(component_mask): 514 | # Find contours 515 | contours, _ = cv2.findContours(component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 516 | if len(contours) != 1: 517 | return False 518 | contour = contours[0] 519 | 520 | # Check if the contour is closed by checking if the start and end points are the same 521 | # if cv2.arcLength(contour, True) > 0: 522 | # return True 523 | # Approximate the contour with polygons. 524 | epsilon = 0.05 * cv2.arcLength(contour, True) 525 | approx = cv2.approxPolyDP(contour, epsilon, True) 526 | 527 | # Check if the polygon is closed (has no gaps) and has a reasonable number of vertices for a ring. 528 | if not cv2.isContourConvex(approx) or len(approx) < 4: 529 | return False 530 | 531 | # All conditions met, likely a close ring. 532 | return True 533 | 534 | # Iterate over each component 535 | ring_count = 0 536 | is_full = False 537 | the_area = 0 538 | for label in range(1, num_labels): 539 | x, y, width, height, area = stats[label, :5] 540 | bounding_box_area = width * height 541 | component_mask = (labels == label).astype(np.uint8) * 255 542 | contours, _ = cv2.findContours(component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 543 | color = colors[label % len(colors)] 544 | cv2.drawContours(image_with_contours, contours, -1, color, 2) 545 | if bounding_box_area >= min_area: 546 | # Select a color from the list based on the label index 547 | if is_full_ring(component_mask): 548 | is_full = True 549 | the_area = area 550 | ring_count += 1 551 | 552 | if self.task.debug: 553 | # Save or display the image with contours 554 | cv2.imwrite(f'test\\test_{self}_{is_full}_{the_area}_{lower_bound}.jpg', image_with_contours) 555 | if ring_count > 1: 556 | is_full = False 557 | the_area = 0 558 | self.logger.warning(f'is_con_full found multiple rings {ring_count}') 559 | 560 | return the_area, is_full 561 | 562 | 563 | forte_white_color = { 564 | 'r': (244, 255), # Red range 565 | 'g': (246, 255), # Green range 566 | 'b': (250, 255) # Blue range 567 | } 568 | 569 | dot_color = { 570 | 'r': (235, 255), # Red range 571 | 'g': (235, 255), # Green range 572 | 'b': (235, 255) # Blue range 573 | } 574 | 575 | con_colors = [ 576 | { 577 | 'r': (205, 235), 578 | 'g': (190, 222), # for yellow spectro 579 | 'b': (90, 130) 580 | }, 581 | { 582 | 'r': (150, 190), # Red range 583 | 'g': (95, 140), # Green range for purple electric 584 | 'b': (210, 249) # Blue range 585 | }, 586 | { 587 | 'r': (200, 230), # Red range 588 | 'g': (100, 130), # Green range for red fire 589 | 'b': (75, 105) # Blue range 590 | }, 591 | { 592 | 'r': (60, 95), # Red range 593 | 'g': (150, 180), # Green range for blue ice 594 | 'b': (210, 245) # Blue range 595 | }, 596 | { 597 | 'r': (70, 110), # Red range 598 | 'g': (215, 250), # Green range for green wind 599 | 'b': (155, 190) # Blue range 600 | }, 601 | { 602 | 'r': (190, 220), # Red range 603 | 'g': (65, 105), # Green range for havoc 604 | 'b': (145, 175) # Blue range 605 | } 606 | ] 607 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc.