diff --git a/.gitignore b/.gitignore index e3596b7e8e..5214700436 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ config/reloadflag config/reloadalas test.py test/ +assets/shop/event_cost # Created by .ignore support plugin (hsz.mobi) diff --git a/assets/cn/shop_event/EVENT_SHOP_DEADLINE.png b/assets/cn/shop_event/EVENT_SHOP_DEADLINE.png new file mode 100644 index 0000000000..d07109ddda Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_DEADLINE.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png b/assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png new file mode 100644 index 0000000000..ee6bd5b896 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png b/assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png new file mode 100644 index 0000000000..1425bedea3 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_PT_SSR.png b/assets/cn/shop_event/EVENT_SHOP_PT_SSR.png new file mode 100644 index 0000000000..dddba37e36 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_PT_SSR.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_PT_UR.png b/assets/cn/shop_event/EVENT_SHOP_PT_UR.png new file mode 100644 index 0000000000..117b4b7ac2 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_PT_UR.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png b/assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png new file mode 100644 index 0000000000..1e5552c117 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png differ diff --git a/assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png b/assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png new file mode 100644 index 0000000000..399ddd5b06 Binary files /dev/null and b/assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png differ diff --git a/assets/cn/ui/SHOP_GOTO_MUNITIONS.png b/assets/cn/ui/SHOP_GOTO_MUNITIONS.png index 5482fb4555..f830bdb9cf 100644 Binary files a/assets/cn/ui/SHOP_GOTO_MUNITIONS.png and b/assets/cn/ui/SHOP_GOTO_MUNITIONS.png differ diff --git a/assets/en/ui/SHOP_GOTO_MUNITIONS.png b/assets/en/ui/SHOP_GOTO_MUNITIONS.png index 5482fb4555..f830bdb9cf 100644 Binary files a/assets/en/ui/SHOP_GOTO_MUNITIONS.png and b/assets/en/ui/SHOP_GOTO_MUNITIONS.png differ diff --git a/assets/jp/ui/SHOP_GOTO_MUNITIONS.png b/assets/jp/ui/SHOP_GOTO_MUNITIONS.png index 1a1c0ad9e8..f830bdb9cf 100644 Binary files a/assets/jp/ui/SHOP_GOTO_MUNITIONS.png and b/assets/jp/ui/SHOP_GOTO_MUNITIONS.png differ diff --git a/assets/shop/event/Array.png b/assets/shop/event/Array.png new file mode 100644 index 0000000000..74f5d2ccd4 Binary files /dev/null and b/assets/shop/event/Array.png differ diff --git a/assets/shop/event/AugmentChangeT1.png b/assets/shop/event/AugmentChangeT1.png new file mode 100644 index 0000000000..f811af1a88 Binary files /dev/null and b/assets/shop/event/AugmentChangeT1.png differ diff --git a/assets/shop/event/AugmentChangeT2.png b/assets/shop/event/AugmentChangeT2.png new file mode 100644 index 0000000000..b650da4c41 Binary files /dev/null and b/assets/shop/event/AugmentChangeT2.png differ diff --git a/assets/shop/event/AugmentCore.png b/assets/shop/event/AugmentCore.png new file mode 100644 index 0000000000..6ea4e44c65 Binary files /dev/null and b/assets/shop/event/AugmentCore.png differ diff --git a/assets/shop/event/AugmentEnhanceT2.png b/assets/shop/event/AugmentEnhanceT2.png new file mode 100644 index 0000000000..a191da09ac Binary files /dev/null and b/assets/shop/event/AugmentEnhanceT2.png differ diff --git a/assets/shop/event/BoxT4.png b/assets/shop/event/BoxT4.png new file mode 100644 index 0000000000..d95b19dc49 Binary files /dev/null and b/assets/shop/event/BoxT4.png differ diff --git a/assets/shop/event/CatT1.png b/assets/shop/event/CatT1.png new file mode 100644 index 0000000000..41adb1cf39 Binary files /dev/null and b/assets/shop/event/CatT1.png differ diff --git a/assets/shop/event/CatT2.png b/assets/shop/event/CatT2.png new file mode 100644 index 0000000000..2d6100d894 Binary files /dev/null and b/assets/shop/event/CatT2.png differ diff --git a/assets/shop/event/CatT3.png b/assets/shop/event/CatT3.png new file mode 100644 index 0000000000..e92cfea5a0 Binary files /dev/null and b/assets/shop/event/CatT3.png differ diff --git a/assets/shop/event/Chip.png b/assets/shop/event/Chip.png new file mode 100644 index 0000000000..ceb2db0483 Binary files /dev/null and b/assets/shop/event/Chip.png differ diff --git a/assets/shop/event/Coin.png b/assets/shop/event/Coin.png new file mode 100644 index 0000000000..6417dd1a16 Binary files /dev/null and b/assets/shop/event/Coin.png differ diff --git a/assets/shop/event/DRS7.png b/assets/shop/event/DRS7.png new file mode 100644 index 0000000000..2d4f7a5c5b Binary files /dev/null and b/assets/shop/event/DRS7.png differ diff --git a/assets/shop/event/DRS7_2.png b/assets/shop/event/DRS7_2.png new file mode 100644 index 0000000000..5ad17e68f5 Binary files /dev/null and b/assets/shop/event/DRS7_2.png differ diff --git a/assets/shop/event/DRS7_3.png b/assets/shop/event/DRS7_3.png new file mode 100644 index 0000000000..8cdb221553 Binary files /dev/null and b/assets/shop/event/DRS7_3.png differ diff --git a/assets/shop/event/FoodT1.png b/assets/shop/event/FoodT1.png new file mode 100644 index 0000000000..00c21c0828 Binary files /dev/null and b/assets/shop/event/FoodT1.png differ diff --git a/assets/shop/event/GachaTicket.png b/assets/shop/event/GachaTicket.png new file mode 100644 index 0000000000..f27b1cc5c4 Binary files /dev/null and b/assets/shop/event/GachaTicket.png differ diff --git a/assets/shop/event/Oil.png b/assets/shop/event/Oil.png new file mode 100644 index 0000000000..850f74202b Binary files /dev/null and b/assets/shop/event/Oil.png differ diff --git a/assets/shop/event/PRS7.png b/assets/shop/event/PRS7.png new file mode 100644 index 0000000000..0d8f169a32 Binary files /dev/null and b/assets/shop/event/PRS7.png differ diff --git a/assets/shop/event/PlateAntiairT3.png b/assets/shop/event/PlateAntiairT3.png new file mode 100644 index 0000000000..7b612691e0 Binary files /dev/null and b/assets/shop/event/PlateAntiairT3.png differ diff --git a/assets/shop/event/PlateGeneralT3.png b/assets/shop/event/PlateGeneralT3.png new file mode 100644 index 0000000000..5cee4aeefd Binary files /dev/null and b/assets/shop/event/PlateGeneralT3.png differ diff --git a/assets/shop/event/PlateGunT3.png b/assets/shop/event/PlateGunT3.png new file mode 100644 index 0000000000..f9f30ff205 Binary files /dev/null and b/assets/shop/event/PlateGunT3.png differ diff --git a/assets/shop/event/PlatePlaneT3.png b/assets/shop/event/PlatePlaneT3.png new file mode 100644 index 0000000000..17f2ac2e1f Binary files /dev/null and b/assets/shop/event/PlatePlaneT3.png differ diff --git a/assets/shop/event/PlateTorpedoT3.png b/assets/shop/event/PlateTorpedoT3.png new file mode 100644 index 0000000000..f19173f70f Binary files /dev/null and b/assets/shop/event/PlateTorpedoT3.png differ diff --git a/assets/tw/ui/SHOP_GOTO_MUNITIONS.png b/assets/tw/ui/SHOP_GOTO_MUNITIONS.png index 5482fb4555..f830bdb9cf 100644 Binary files a/assets/tw/ui/SHOP_GOTO_MUNITIONS.png and b/assets/tw/ui/SHOP_GOTO_MUNITIONS.png differ diff --git a/config/template.json b/config/template.json index bd93fe74d4..5a71d59bae 100644 --- a/config/template.json +++ b/config/template.json @@ -1466,6 +1466,13 @@ "CoreShop": { "Filter": "Array" }, + "EventShop": { + "Enable": false, + "UnlockShipSSR": false, + "BuyShipUR": 0, + "PresetFilter": "all", + "CustomFilter": "EquipUR > EquipSSR > GachaTicket\n> DR > PR > Array > Chip > CatT3\n> Meta > Skinbox\n> Oil > Coin > FoodT1\n> AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1\n> CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4\n> ShipSSR" + }, "Storage": { "Storage": {} } diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 8d9b66a9c0..39d87db999 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -7847,6 +7847,37 @@ "value": "Array" } }, + "EventShop": { + "Enable": { + "type": "checkbox", + "value": false + }, + "UnlockShipSSR": { + "type": "checkbox", + "value": false + }, + "BuyShipUR": { + "type": "select", + "value": 0, + "option": [ + 0, + 1, + 2 + ] + }, + "PresetFilter": { + "type": "select", + "value": "all", + "option": [ + "all", + "custom" + ] + }, + "CustomFilter": { + "type": "textarea", + "value": "EquipUR > EquipSSR > GachaTicket\n> DR > PR > Array > Chip > CatT3\n> Meta > Skinbox\n> Oil > Coin > FoodT1\n> AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1\n> CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4\n> ShipSSR" + } + }, "Storage": { "Storage": { "type": "storage", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 9b48894914..3ce74a9233 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -530,6 +530,25 @@ MeritShop: CoreShop: Filter: |- Array +EventShop: + Enable: false + UnlockShipSSR: false + BuyShipUR: + value: 0 + option: [ 0, 1, 2 ] + PresetFilter: + value: all + option: + - all + - custom + CustomFilter: |- + EquipUR > EquipSSR > GachaTicket + > DR > PR > Array > Chip > CatT3 + > Meta > Skinbox + > Oil > Coin > FoodT1 + > AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1 + > CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4 + > ShipSSR ShipyardDr: ResearchSeries: value: 2 diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 74e40c0883..db7f7a16aa 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -240,6 +240,7 @@ DailyMission: - MedalShop2 - MeritShop - CoreShop + - EventShop Shipyard: - Scheduler - ShipyardDr diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 2200caec34..68a1a8099b 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -289,6 +289,13 @@ class GeneratedConfig: # Group `CoreShop` CoreShop_Filter = 'Array' + # Group `EventShop` + EventShop_Enable = False + EventShop_UnlockShipSSR = False + EventShop_BuyShipUR = 0 # 0, 1, 2 + EventShop_PresetFilter = 'all' # all, custom + EventShop_CustomFilter = 'EquipUR > EquipSSR > GachaTicket\n> DR > PR > Array > Chip > CatT3\n> Meta > Skinbox\n> Oil > Coin > FoodT1\n> AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1\n> CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4\n> ShipSSR' + # Group `ShipyardDr` ShipyardDr_ResearchSeries = 2 # 2, 3 ShipyardDr_ShipIndex = 0 # 0, 1, 2, 3, 4, 5, 6 diff --git a/module/config/config_manual.py b/module/config/config_manual.py index 46746053fd..a4abd98792 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -364,6 +364,11 @@ def SERVER(self): # For dev purpose, auto extract new item templates SHOP_EXTRACT_TEMPLATE = False + """ + module.event_shop + """ + EVENT_SHOP_IGNORE_DEADLINE = False + """ module.war_archives """ diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 9ce7448e98..eb9685393b 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -1801,6 +1801,37 @@ "help": "All options have been defined at \nHowever unlike other shops, only Chip and Array are supported\nALAS does not browse, scroll, or recognize any other items displayed besides those two" } }, + "EventShop": { + "_info": { + "name": "Event Shop", + "help": "Ships will be bought after event ends, and corresponding pts will be preserved." + }, + "Enable": { + "name": "Enable Event Shop", + "help": "" + }, + "UnlockShipSSR": { + "name": "Unlock event SSR ship", + "help": "If not enabled, will discard `ShipSSR` and `Meta` in filter string." + }, + "BuyShipUR": { + "name": "Buy X event UR ship", + "help": "", + "0": "0", + "1": "1", + "2": "2" + }, + "PresetFilter": { + "name": "Preset Filter Select", + "help": "", + "all": "all", + "custom": "custom" + }, + "CustomFilter": { + "name": "Custom Filter", + "help": "To use your own filter, set \"Preset Filter Select\" to \"custom\". All options have been defined at " + } + }, "ShipyardDr": { "_info": { "name": "DR Blueprints Purchase Settings", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index c5827cda0a..f756d9c060 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -1801,6 +1801,37 @@ "help": "CoreShop.Filter.help" } }, + "EventShop": { + "_info": { + "name": "EventShop._info.name", + "help": "EventShop._info.help" + }, + "Enable": { + "name": "EventShop.Enable.name", + "help": "EventShop.Enable.help" + }, + "UnlockShipSSR": { + "name": "EventShop.UnlockShipSSR.name", + "help": "EventShop.UnlockShipSSR.help" + }, + "BuyShipUR": { + "name": "EventShop.BuyShipUR.name", + "help": "EventShop.BuyShipUR.help", + "0": "0", + "1": "1", + "2": "2" + }, + "PresetFilter": { + "name": "EventShop.PresetFilter.name", + "help": "EventShop.PresetFilter.help", + "all": "all", + "custom": "custom" + }, + "CustomFilter": { + "name": "EventShop.CustomFilter.name", + "help": "EventShop.CustomFilter.help" + } + }, "ShipyardDr": { "_info": { "name": "ShipyardDr._info.name", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index b5e8888cc5..bcc349c6a1 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -1801,6 +1801,37 @@ "help": "" } }, + "EventShop": { + "_info": { + "name": "活动商店", + "help": "活动船只将会在活动结束后购买" + }, + "Enable": { + "name": "启用活动商店", + "help": "" + }, + "UnlockShipSSR": { + "name": "解锁活动金船", + "help": "如果不启用,将会抛掉过滤器中的ShipSSR和Meta" + }, + "BuyShipUR": { + "name": "购买X只活动彩船", + "help": "", + "0": "0", + "1": "1", + "2": "2" + }, + "PresetFilter": { + "name": "活动商店过滤器", + "help": "", + "all": "全部", + "custom": "自定义" + }, + "CustomFilter": { + "name": "自定义过滤器", + "help": "使用自定义过滤器需将 \"活动商店过滤器\" 设置为 \"自定义\",并阅读 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/reward_shop_filter_string" + } + }, "ShipyardDr": { "_info": { "name": "彩科研图纸购买设置", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 311590977b..3cf182d5bb 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -1801,6 +1801,37 @@ "help": "" } }, + "EventShop": { + "_info": { + "name": "活動商店", + "help": "活動船只將會在活動結束後購買" + }, + "Enable": { + "name": "啟用活動商店", + "help": "" + }, + "UnlockShipSSR": { + "name": "解鎖活動金船", + "help": "如果不啟用,將會拋掉過濾器中的ShipSSR和Meta" + }, + "BuyShipUR": { + "name": "購買X只活動彩船", + "help": "", + "0": "0", + "1": "1", + "2": "2" + }, + "PresetFilter": { + "name": "活動商店過濾器", + "help": "", + "all": "全部", + "custom": "自定義" + }, + "CustomFilter": { + "name": "自定義過濾器", + "help": "使用自定義過濾器需將 \"活動商店過濾器\" 設定為 \"自定義\",並閱讀 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/reward_shop_filter_string" + } + }, "ShipyardDr": { "_info": { "name": "彩科研圖紙購買設定", diff --git a/module/ocr/ocr.py b/module/ocr/ocr.py index d245df7541..a8d0efc062 100644 --- a/module/ocr/ocr.py +++ b/module/ocr/ocr.py @@ -140,14 +140,14 @@ class Digit(Ocr): Method ocr() returns int, or a list of int. """ - def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789IDSB', + def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789IDSBZ', name=None): super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) def after_process(self, result): result = super().after_process(result) result = result.replace('I', '1').replace('D', '0').replace('S', '5') - result = result.replace('B', '8') + result = result.replace('B', '8').replace('Z', '2') prev = result result = int(result) if result else 0 @@ -163,14 +163,14 @@ class DigitYuv(Digit, OcrYuv): class DigitCounter(Ocr): - def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789/IDSB', + def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789/IDSBZ', name=None): super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) def after_process(self, result): result = super().after_process(result) result = result.replace('I', '1').replace('D', '0').replace('S', '5') - result = result.replace('B', '8') + result = result.replace('B', '8').replace('Z', '2') return result def ocr(self, image, direct_ocr=False): @@ -204,14 +204,14 @@ class DigitCounterYuv(DigitCounter, OcrYuv): class Duration(Ocr): - def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789:IDSB', + def __init__(self, buttons, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='0123456789:IDSBZ', name=None): super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) def after_process(self, result): result = super().after_process(result) result = result.replace('I', '1').replace('D', '0').replace('S', '5') - result = result.replace('B', '8') + result = result.replace('B', '8').replace('Z', '2') return result def ocr(self, image, direct_ocr=False): diff --git a/module/shop/shop_reward.py b/module/shop/shop_reward.py index ed2a407ad5..e92c4f7a4b 100644 --- a/module/shop/shop_reward.py +++ b/module/shop/shop_reward.py @@ -4,6 +4,8 @@ from module.shop.shop_medal import MedalShop2 from module.shop.shop_merit import MeritShop from module.shop.ui import ShopUI +from module.shop_event.shop_event import EventShop +from module.shop_event.ui import OCR_EVENT_SHOP_SECOND_ENSURE class RewardShop(ShopUI): @@ -19,7 +21,16 @@ def run_frequent(self): def run_once(self): # Munitions shops - self.ui_goto_shop() + if self.config.EventShop_Enable: + self.ui_goto_event_shop() + if self.shop_tab.get_active(main=self) == 2: + EventShop(self.config, self.device).run() + text = OCR_EVENT_SHOP_SECOND_ENSURE.ocr(self.device.image) + if text != "": + self.shop_nav.set(main=self, upper=2) + EventShop(self.config, self.device).run() + else: + self.ui_goto_shop() self.shop_tab.set(main=self, left=2) self.shop_nav.set(main=self, upper=2) diff --git a/module/shop/ui.py b/module/shop/ui.py index 78c52af7d9..5ba629439d 100644 --- a/module/shop/ui.py +++ b/module/shop/ui.py @@ -4,9 +4,9 @@ from module.handler.assets import POPUP_CONFIRM from module.logger import logger from module.shop.assets import * -from module.ui.assets import ACADEMY_GOTO_MUNITIONS, BACK_ARROW +from module.ui.assets import ACADEMY_GOTO_MUNITIONS, BACK_ARROW, SHOP_GOTO_MUNITIONS from module.ui.navbar import Navbar -from module.ui.page import page_academy, page_munitions +from module.ui.page import page_academy, page_main, page_shop, page_munitions from module.ui.ui import UI @@ -57,10 +57,11 @@ def shop_tab(self): - index 1: Monthly shops 2: General supply shops + 3: Event shops """ grids = ButtonGrid( origin=(340, 93), delta=(189, 0), - button_shape=(188, 54), grid_shape=(2, 1), + button_shape=(188, 54), grid_shape=(3, 1), name='SHOP_TAB') return Navbar( grids=grids, @@ -85,6 +86,9 @@ def shop_nav(self): 3: Guild shop 4: Meta shop 5: Gift shop + - index when `shop_tab` is at 3 + 1: Current event shop + 2: Previous event shop (if exists) """ grids = ButtonGrid( origin=(339, 217), delta=(0, 65), @@ -237,3 +241,33 @@ def ui_goto_shop(self): # Large offset cause it camera in academy can be move around if self.appear_then_click(ACADEMY_GOTO_MUNITIONS, offset=(200, 200), interval=5): continue + + def ui_goto_event_shop(self): + """ + Goes to page_munitions + This route guarantees start + in event shop if exists + + Pages: + in: Any + out: page_munitions + """ + if self.ui_get_current_page() == page_munitions\ + and self.shop_tab.get_active(main=self) == 2: + logger.info(f'Already at {page_munitions}') + return + + self.ui_ensure(page_shop) + + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear(page_munitions.check_button, offset=(20, 20)): + break + + if self.appear_then_click(SHOP_GOTO_MUNITIONS, offset=(20, 20), interval=5): + continue diff --git a/module/shop_event/assets.py b/module/shop_event/assets.py new file mode 100644 index 0000000000..7ee0b3b0cf --- /dev/null +++ b/module/shop_event/assets.py @@ -0,0 +1,13 @@ +from module.base.button import Button +from module.base.template import Template + +# This file was automatically generated by dev_tools/button_extract.py. +# Don't modify it manually. + +EVENT_SHOP_DEADLINE = Button(area={'cn': (437, 192, 761, 216), 'en': (437, 192, 761, 216), 'jp': (437, 192, 761, 216), 'tw': (437, 192, 761, 216)}, color={'cn': (76, 75, 60), 'en': (76, 75, 60), 'jp': (76, 75, 60), 'tw': (76, 75, 60)}, button={'cn': (437, 192, 761, 216), 'en': (437, 192, 761, 216), 'jp': (437, 192, 761, 216), 'tw': (437, 192, 761, 216)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_DEADLINE.png', 'en': './assets/cn/shop_event/EVENT_SHOP_DEADLINE.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_DEADLINE.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_DEADLINE.png'}) +EVENT_SHOP_ITEM_REMAIN = Button(area={'cn': (442, 231, 490, 250), 'en': (442, 231, 490, 250), 'jp': (442, 231, 490, 250), 'tw': (442, 231, 490, 250)}, color={'cn': (146, 150, 78), 'en': (146, 150, 78), 'jp': (146, 150, 78), 'tw': (146, 150, 78)}, button={'cn': (442, 231, 490, 250), 'en': (442, 231, 490, 250), 'jp': (442, 231, 490, 250), 'tw': (442, 231, 490, 250)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png', 'en': './assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_ITEM_REMAIN.png'}) +EVENT_SHOP_LOAD_ENSURE = Button(area={'cn': (492, 374, 523, 405), 'en': (492, 374, 523, 405), 'jp': (492, 374, 523, 405), 'tw': (492, 374, 523, 405)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (492, 374, 523, 405), 'en': (492, 374, 523, 405), 'jp': (492, 374, 523, 405), 'tw': (492, 374, 523, 405)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png', 'en': './assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_LOAD_ENSURE.png'}) +EVENT_SHOP_PT_SSR = Button(area={'cn': (1163, 174, 1261, 198), 'en': (1163, 174, 1261, 198), 'jp': (1163, 174, 1261, 198), 'tw': (1163, 174, 1261, 198)}, color={'cn': (88, 91, 101), 'en': (88, 91, 101), 'jp': (88, 91, 101), 'tw': (88, 91, 101)}, button={'cn': (1163, 174, 1261, 198), 'en': (1163, 174, 1261, 198), 'jp': (1163, 174, 1261, 198), 'tw': (1163, 174, 1261, 198)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_PT_SSR.png', 'en': './assets/cn/shop_event/EVENT_SHOP_PT_SSR.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_PT_SSR.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_PT_SSR.png'}) +EVENT_SHOP_PT_UR = Button(area={'cn': (911, 171, 1008, 200), 'en': (911, 171, 1008, 200), 'jp': (911, 171, 1008, 200), 'tw': (911, 171, 1008, 200)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (911, 171, 1008, 200), 'en': (911, 171, 1008, 200), 'jp': (911, 171, 1008, 200), 'tw': (911, 171, 1008, 200)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_PT_UR.png', 'en': './assets/cn/shop_event/EVENT_SHOP_PT_UR.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_PT_UR.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_PT_UR.png'}) +EVENT_SHOP_SCROLL_AREA = Button(area={'cn': (1260, 221, 1266, 643), 'en': (1260, 221, 1266, 643), 'jp': (1260, 221, 1266, 643), 'tw': (1260, 221, 1266, 643)}, color={'cn': (242, 205, 66), 'en': (242, 205, 66), 'jp': (242, 205, 66), 'tw': (242, 205, 66)}, button={'cn': (1260, 221, 1266, 643), 'en': (1260, 221, 1266, 643), 'jp': (1260, 221, 1266, 643), 'tw': (1260, 221, 1266, 643)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png', 'en': './assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_SCROLL_AREA.png'}) +EVENT_SHOP_SECOND_ENSURE = Button(area={'cn': (339, 302, 442, 328), 'en': (339, 302, 442, 328), 'jp': (339, 302, 442, 328), 'tw': (339, 302, 442, 328)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (339, 302, 442, 328), 'en': (339, 302, 442, 328), 'jp': (339, 302, 442, 328), 'tw': (339, 302, 442, 328)}, file={'cn': './assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png', 'en': './assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png', 'jp': './assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png', 'tw': './assets/cn/shop_event/EVENT_SHOP_SECOND_ENSURE.png'}) diff --git a/module/shop_event/clerk.py b/module/shop_event/clerk.py new file mode 100644 index 0000000000..d9fe25c826 --- /dev/null +++ b/module/shop_event/clerk.py @@ -0,0 +1,341 @@ +import cv2 +import os + +from module.base.button import ButtonGrid +from module.base.decorator import cached_property +from module.base.template import Template +from module.base.utils import save_image +from module.combat.assets import GET_ITEMS_1, GET_ITEMS_3, GET_SHIP +from module.shop_event.item import EventShopItemGrid +from module.shop_event.ui import EventShopUI, EVENT_SHOP_SCROLL, OCR_EVENT_SHOP_ITEM_REMAIN +from module.map_detection.utils import Points +from module.logger import logger +from module.shop.assets import AMOUNT_MAX, AMOUNT_MINUS, AMOUNT_PLUS, SHOP_BUY_CONFIRM, SHOP_BUY_CONFIRM_AMOUNT, SHOP_CLICK_SAFE_AREA +from module.shop.clerk import OCR_SHOP_AMOUNT +from module.ui.assets import BACK_ARROW + + +class EventShopClerk(EventShopUI): + """ + Class for Event Shop operations containing UI and items. + """ + + def record_event_shop_cost(self): + """ + Record event pt icon for itemgrid detection. + Uses pt icon on the upper-right part for templates. + """ + logger.hr('Record Event Pt Icon', level=2) + + os.makedirs(os.path.dirname('./assets/shop/event_cost/'), exist_ok=True) + scaling = 5/6 + if self.event_shop_has_pt_ur: + pt_ssr_icon = self.image_crop((820, 172, 844, 196), copy=False) + pt_ur_icon = self.image_crop((1036, 172, 1060, 196), copy=False) + pt_ur_icon = cv2.resize(pt_ur_icon, None, fx=scaling, fy=scaling, interpolation=cv2.INTER_AREA) + save_image(pt_ur_icon, './assets/shop/event_cost/URPt.png') + else: + pt_ssr_icon = self.image_crop((1036, 172, 1060, 196), copy=False) + + pt_ssr_icon = cv2.resize(pt_ssr_icon, None, fx=scaling, fy=scaling, interpolation=cv2.INTER_AREA) + save_image(pt_ssr_icon, './assets/shop/event_cost/Pt.png') + + def _get_event_shop_cost(self): + """ + Returns: + np.array: [[x1, y1], [x2, y2]], location of the pt icon upper-left corner. + """ + image = self.image_crop((472, 348, 1170, 625), copy=False) + similarity = 0.5 + + TEMPLATE_PT_SSR_ICON = Template('./assets/shop/event_cost/Pt.png') + result = TEMPLATE_PT_SSR_ICON.match_multi(image, similarity=similarity, threshold=15) + if self.event_shop_has_pt_ur: + TEMPLATE_PT_UR_ICON = Template('./assets/shop/event_cost/URPt.png') + result += TEMPLATE_PT_UR_ICON.match_multi(image, similarity=similarity, threshold=15) + offsets = [(res.area[0] % 156, res.area[1] % 213) for res in result] + offset = max(set(offsets), key=offsets.count, default=(0, 0)) + result = [res for res in result + if abs(res.area[0] % 156 - offset[0]) < 5 and abs(res.area[1] % 213 - offset[1]) < 5] + logger.attr('Costs', f'{result}') + return Points([(0., p.area[1]) for p in result]).group(threshold=5) + + @cached_property + def event_shop_items(self): + event_shop_items = EventShopItemGrid( + grids=None, templates={}, template_area=(10, 10, 89, 70), + amount_area=(60, 71, 96, 97), price_area=(52, 132, 130, 165), + tag_area=(0, 74, 1, 92), counter_area=(70, 167, 134, 186), + ) + event_shop_items.load_template_folder('./assets/shop/event') + event_shop_items.load_cost_template_folder('./assets/shop/event_cost') + return event_shop_items + + def _get_event_shop_grid(self): + """ + Returns shop grid. + + Returns: + ButtonGrid: + """ + costs = self._get_event_shop_cost() + row = len(costs) + y = 0 + delta_y = 0 + + if row == 1: + y = costs[0][1] + 348 - (379 - 246) + delta_y = 213 + elif row == 2: + y = min(costs[0][1], costs[1][1]) + 348 - (379 - 246) + delta_y = abs(costs[0][1] - costs[1][1]) + + shop_grid = ButtonGrid( + origin=(476, y), delta=(156, delta_y), button_shape=(98, 98), grid_shape=(5, row), name='EVENT_SHOP_GRID' + ) + return shop_grid + + def event_shop_get_items(self, scroll_pos=None): + """ + Args: + scroll_pos (Float): Additional scroll position. + + Returns: + list[Item]: + """ + self.event_shop_items.grids = self._get_event_shop_grid() + if self.config.SHOP_EXTRACT_TEMPLATE: + self.event_shop_items.extract_template(self.device.image, './assets/shop/event') + self.event_shop_items.predict(self.device.image, counter=True, scroll_pos=scroll_pos) + shop_items = self.event_shop_items.items + + if len(shop_items): + min_row = self.event_shop_items.grids[0, 0].area[1] + row = [str(item) for item in shop_items if item.button[1] == min_row] + logger.info(f'Shop row 1: {row}') + row = [str(item) for item in shop_items if item.button[1] != min_row] + logger.info(f'Shop row 2: {row}') + return shop_items + else: + logger.info('No shop items found') + return [] + + def scan_all(self): + """ + Returns: + list[EventShopItem]: + """ + items = [] + self.device.click_record.clear() + + logger.hr('Event Shop Scan', level=2) + pre_pos, cur_pos = self.init_slider() + + while 1: + pre_pos = self.pre_scroll(pre_pos, cur_pos) + _items = self.event_shop_get_items(cur_pos) + + for _ in range(2): + if not len(_items) or any(not item.is_known_item() for item in _items): + logger.warning('Empty Event shop or empty items, confirming') + self.device.sleep((0.3, 0.5)) + self.device.screenshot() + _items = self.event_shop_get_items(cur_pos) + continue + else: + items += _items + logger.info(f'Found {len(_items)} items at pos {cur_pos:.2f}') + break + + if EVENT_SHOP_SCROLL.at_bottom(main=self): + logger.info('Event shop reach bottom, stop') + break + else: + EVENT_SHOP_SCROLL.next_page(main=self, page=0.66) + cur_pos = EVENT_SHOP_SCROLL.cal_position(main=self) + continue + self.device.click_record.clear() + + return items + + def event_shop_obstruct_handle(self): + """ + Remove obstructions in shop view if any + + Returns: + bool: + """ + # Handle shop obstructions + if self.appear(GET_SHIP, interval=1): + logger.info(f'Shop obstruct: {GET_SHIP} -> {SHOP_CLICK_SAFE_AREA}') + self.device.click(SHOP_CLICK_SAFE_AREA) + return True + # To lock new ships + if self.handle_popup_confirm('SHOP_OBSTRUCT'): + return True + if self.appear(GET_ITEMS_1, interval=1): + logger.info(f'Shop obstruct: {GET_ITEMS_1} -> {SHOP_CLICK_SAFE_AREA}') + self.device.click(SHOP_CLICK_SAFE_AREA) + return True + if self.appear(GET_ITEMS_3, interval=1): + logger.info(f'Shop obstruct: {GET_ITEMS_3} -> {SHOP_CLICK_SAFE_AREA}') + self.device.click(SHOP_CLICK_SAFE_AREA) + return True + + return False + + def event_shop_handle_amount(self, item, amount=None, skip_first_screenshot=True): + count = item.count + logger.attr("Item_count", count) + + MAX_AMOUNT = { + "Coin": 600000, + "Oil": 25000 + } + if item.name in MAX_AMOUNT.keys(): + current = OCR_EVENT_SHOP_ITEM_REMAIN.ocr(self.device.image) + limit = int((MAX_AMOUNT[item.name] - current) // item.price) + if count > limit: + logger.info(f"Item hard limit: {MAX_AMOUNT[item.name]}, Current stock: {current}") + logger.info(f"Can buy: {limit}") + count = limit + + if item.cost == "Pt": + count = min(count, int((self._pt - self.pt_preserve)// item.price)) + logger.info(f"should buy: {count}") + else: + count = min(count, int(self._pt_ur // item.price)) + logger.info(f"should buy: {count}") + + if count == 1: + return True + elif count == 0: + return False + + self.interval_clear(AMOUNT_MAX) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear_then_click(AMOUNT_MAX, offset=(50, 50), interval=3): + continue + + if OCR_SHOP_AMOUNT.ocr(self.device.image) > 1: + break + + if amount is not None: + self.ui_ensure_index(amount, letter=OCR_SHOP_AMOUNT, prev_button=AMOUNT_MINUS, next_button=AMOUNT_PLUS, + skip_first_screenshot=True) + logger.info(f"Set buy count to {amount}") + else: + self.ui_ensure_index(count, letter=OCR_SHOP_AMOUNT, prev_button=AMOUNT_MINUS, next_button=AMOUNT_PLUS, + skip_first_screenshot=True) + logger.info(f"Set buy count to {count}") + + return True + + def event_shop_buy_item(self, item, amount=None, skip_first_screenshot=True): + success = False + amount_finish = False + self.interval_clear(SHOP_BUY_CONFIRM) + self.interval_clear(SHOP_BUY_CONFIRM_AMOUNT) + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if self.event_shop_obstruct_handle(): + self.interval_reset(BACK_ARROW) + success = True + continue + if self.appear_then_click(SHOP_BUY_CONFIRM, offset=(50, 50), interval=3): # enlarge offset for event skin confirm button + self.interval_reset(SHOP_BUY_CONFIRM) + continue + if not amount_finish and self.appear(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20)): + handled = self.event_shop_handle_amount(item, amount) + if handled: + amount_finish = True + continue + else: + while 1: + self.device.screenshot() + if self.appear(BACK_ARROW, offset=(20, 20), interval=5): + break + self.device.click(SHOP_CLICK_SAFE_AREA) + logger.warning(f'Cannot buy this item: {item.name}, please check your item storage is not full.') + return False + if amount_finish and self.appear_then_click(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20), interval=3): + self.interval_reset(SHOP_BUY_CONFIRM_AMOUNT) + success = True + continue + if self.handle_popup_confirm('SHOP_BUY'): + continue + if not success and self.appear(BACK_ARROW, offset=(20, 20), interval=5): + amount_finish = False + self.device.click(item) + continue + # End + if success and self.appear(BACK_ARROW, offset=(20, 20)): + break + return success + + def event_shop_get_items_to_buy(self, name, price, tag): + """ + Args: + name (str): Item name. + price (int): Item price. + + Returns: + EventShopItem: + """ + items = self.event_shop_get_items() + for _ in range(2): + if not len(items) or any(not item.is_known_item() for item in items): + logger.warning('Empty Event shop or empty items, confirming') + self.device.sleep((0.3, 0.5)) + self.device.screenshot() + items = self.event_shop_get_items() + continue + else: + _items = [item for item in items if item.name == name and item.price == price and item.tag == tag] + if len(_items): + return _items.pop() + return None + + def event_shop_buy(self, item, amount=None, skip_first_screenshot=True): + """ + Args: + item: Item to buy + + Returns: + bool: if bought + """ + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if self.event_shop_obstruct_handle(): + self.interval_reset(BACK_ARROW) + continue + if self.appear(BACK_ARROW, interval=5): + break + + self.device.click_record.clear() + EVENT_SHOP_SCROLL.set(item.scroll_pos, main=self, skip_first_screenshot=skip_first_screenshot) + _item = self.event_shop_get_items_to_buy(name=item.name, price=item.price, tag=item.tag) + if _item is None: + logger.warning(f'Item {item.name} not found at pos {item.scroll_pos:.2f}, skip.') + return True + elif self.event_shop_buy_item(_item, amount=amount, skip_first_screenshot=False): + logger.info(f'Bought item: {_item.name}.') + return True + else: + logger.info(f'Buying item: {_item.name} failed, possibly because you have too much of this item.') + # This will mostly happen when buying oils. + self.pt_preserve += _item.price * _item.count + logger.attr("Pt_preserve", self.pt_preserve) + self.device.click_record.clear() diff --git a/module/shop_event/item.py b/module/shop_event/item.py new file mode 100644 index 0000000000..855d5a40fa --- /dev/null +++ b/module/shop_event/item.py @@ -0,0 +1,203 @@ +import cv2 +import numpy as np + +from module.base.utils import color_similar +from module.logger import logger +from module.ocr.ocr import Digit, Ocr +from module.statistics.item import Item, ItemGrid + + +class CounterOcr(Ocr): + def __init__(self, buttons, lang='azur_lane', + letter=(255, 255, 255), + threshold=128, + alphabet='0123456789/IDSB@OQZl]i', + name=None): + super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) + + def pre_process(self, image): + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + # add contrast to the image for better ocr results + cv2.convertScaleAbs(image, alpha=1.5, beta=-64, dst=image) + return image + + def after_process(self, result): + # print(result) + result = super().after_process(result) + result = result.replace('I', '1').replace('D', '0').replace('S', '5') + result = result.replace('B', '8').replace('Z', '2') + result = result.replace('@', '0').replace('O', '0').replace('Q', '0') + result = result.replace('l', '1').replace(']', '1').replace('i', '1') + return result + + def ocr(self, image, direct_ocr=False): + """ + Do OCR on a counter, such as `14/15`, and returns 14, 15 + + Args: + image: + direct_ocr: + + Returns: + list[list[int]: [[current, total]]. + """ + result = super().ocr(image, direct_ocr=direct_ocr) + # if something goes wrong here, for example '/1', + # falls back to 1/1. + if isinstance(result, list): + result_list = [] + for i in result: + try: + current, total = [int(j) for j in i.split('/')] + result_list.append([current, total]) + except ValueError: + logger.warning(f'Ocr result {i} is revised to 1/1') + result_list.append([1, 1]) + return result_list + else: + try: + current, total = [int(i) for i in result.split('/')] + except ValueError: + logger.warning(f'Ocr result {result} is revised to 1/1') + current = 1 + total = 1 + finally: + return [current, total] + +COUNTER_OCR = CounterOcr([], lang='cnocr', name='Counter_ocr') + + +class EventShopItem(Item): + # mainly used to distinguish equip skin box and event equip + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._scroll_pos = None + self.total_count = 1 + self.count = 1 + + @property + def scroll_pos(self): + return self._scroll_pos + + @scroll_pos.setter + def scroll_pos(self, value): + self._scroll_pos = value + + def __str__(self): + if self.name != 'DefaultItem' and self.cost == 'DefaultCost': + name = f'{self.name}_x{self.amount}' + elif self.name == 'DefaultItem' and self.cost != 'DefaultCost': + name = f'{self.cost}_x{self.price}' + elif self.name.isdigit(): + name = f'{self.name}_{self.count}/{self.total_count}_{self.cost}_x{self.price}' + else: + name = f'{self.name}_x{self.amount}_{self.cost}_x{self.price}' + + if self.tag is not None: + name = f'{name}_{self.tag}' + + return name + + def __eq__(self, other): + return id(self) == id(other) + + def identify_name(self): + if not self.name.isdigit(): + return + elif self.price == 8000 and self.cost == "Pt": + self.name = "ShipSSR" + elif self.price in [200, 300] and self.cost == "URPt": + self.name = "ShipUR" + elif self.price == 2000 and self.cost == "Pt": + if self.total_count == 4: + self.name = "Meta" + elif self.total_count == 10: + self.name = "SkinBox" + elif self.total_count == 1: + self.name = "EquipSSR" + elif self.price == 10000 and self.cost == "URPt": + self.name = "EquipUR" + elif self.price == 150 and self.cost == "Pt" and self.total_count == 500: + self.name = "PtUR" + else: + if self.cost == "Pt": + self.name = "EquipSSR" + elif self.cost == "URPt": + self.name = "EquipUR" + + +class EventShopItemGrid(ItemGrid): + item_class = EventShopItem + cost_similarity = 0.5 + # similarity = 0.95 + extract_similarity = 0.95 + + def __init__(self, grids, templates, template_area=(40, 21, 89, 70), amount_area=(60, 71, 96, 97), + cost_area=(6, 123, 84, 166), price_area=(52, 132, 132, 156), tag_area=(0, 74, 1, 92), + counter_area=(80, 170, 138, 190)): + super().__init__(grids, templates, template_area, amount_area, cost_area, price_area, tag_area) + self.counter_ocr = COUNTER_OCR + self.counter_area = counter_area + + def match_cost_template(self, item): + """ + Overwrite ItemGrid.match_cost_template. + + Returns: + str: Template name = 'Pt' or 'URPt'. + """ + image = item.crop(self.cost_area) + names = np.array(list(self.cost_templates.keys()))[np.argsort(list(self.cost_templates_hit.values()))][::-1] + for name in names: + if not name in ["Pt", "URPt"]: + continue + + res = cv2.matchTemplate(image, self.cost_templates[name], cv2.TM_CCOEFF_NORMED) + _, similarity, _, _ = cv2.minMaxLoc(res) + if similarity > self.cost_similarity: + self.cost_templates_hit[name] += 1 + return name + + return None + + @staticmethod + def predict_tag(image): + """ + Args: + image (np.ndarray): The tag_area of the item. + + Returns: + str: Tags are like `unobtained`. Default to None + """ + threshold = 50 + color = cv2.mean(np.array(image))[:3] + if color_similar(color1=color, color2=(255, 89, 90), threshold=threshold): + # red + return 'unobtained' + else: + return None + + def predict(self, image, counter=True, scroll_pos=None): + super().predict(image, name=True, amount=True, cost=True, price=True, tag=True) + + # temporary code to distinguish between DR and PR. Shit code. + for item in self.items: + if item.name.startswith('DR') and item.price == 500: + item.name = 'P' + item.name[1:] + if item.name.startswith('PR') and item.price == 1000: + item.name = 'D' + item.name[1:] + + if counter and len(self.items): + counter_list = [item.crop(self.counter_area) for item in self.items] + counter_list = self.counter_ocr.ocr(counter_list, direct_ocr=True) + for i, t in zip(self.items, counter_list): + i.count, i.total_count = t + + if isinstance(scroll_pos, float) and len(self.items): + for i in self.items: + i.scroll_pos = scroll_pos + + for item in self.items: + item.identify_name() + + return self.items diff --git a/module/shop_event/selector.py b/module/shop_event/selector.py new file mode 100644 index 0000000000..5632bfc228 --- /dev/null +++ b/module/shop_event/selector.py @@ -0,0 +1,67 @@ +import re + +from module.base.filter import Filter +from module.config.config_generated import GeneratedConfig + +FILTER_REGEX = re.compile( + '^(ship|equip|pt|gachaticket' + '|meta|skinbox' + '|array|chip|cat|pr|dr' + '|augment' + '|box|plate|coin|oil|food' + ')' + + '(ur|ssr' + '|core|change|enhance' + '|general|gun|torpedo|antiair|plane)?' + + '(s[1-7]|t[1-6])?$' +) +FILTER_ATTR = ('group', 'sub_genre', 'tier') +FILTER = Filter(FILTER_REGEX, FILTER_ATTR) + + +EVENT_SHOP = { + 'all': """ + EquipUR > EquipSSR > GachaTicket + > DR > PR > Array > Chip > CatT3 + > Meta > SkinBox + > Oil > Coin > FoodT1 + > AugmentCore > AugmentEnhanceT2 > AugmentChangeT2 > AugmentChangeT1 + > CatT2 > CatT1 > PlateGeneralT3 > PlateT3 > BoxT4 + > ShipSSR + """, +} + +class EventShopSelector: + def items_get_items(self, items, name, cost="Pt"): + _items = [] + for item in items: + if item.name == name and item.cost == cost: + _items.append(item) + return _items + + def pretreatment(self, items): + _items = [] + for item in items: + item.group, item.sub_genre, item.tier = None, None, None + result = re.search(FILTER_REGEX, item.name.lower()) + if result: + item.group, item.sub_genre, item.tier = [group.lower() + if group is not None else None + for group in result.groups()] + _items.append(item) + return _items + + def items_filter_in_event_shop(self, items): + items = self.pretreatment(items) + preset = self.config.EventShop_PresetFilter + parser = '' + if preset == 'custom': + parser = self.config.EventShop_CustomFilter + if not parser.strip(): + parser = EVENT_SHOP[GeneratedConfig.EventShop_PresetFilter] + else: + parser = EVENT_SHOP[preset] + FILTER.load(parser) + return FILTER.apply(items) \ No newline at end of file diff --git a/module/shop_event/shop_event.py b/module/shop_event/shop_event.py new file mode 100644 index 0000000000..fb1ea55448 --- /dev/null +++ b/module/shop_event/shop_event.py @@ -0,0 +1,189 @@ +from module.logger import logger +from module.shop_event.clerk import EventShopClerk +from module.shop_event.selector import EventShopSelector + + +class EventShop(EventShopClerk, EventShopSelector): + """ + Class for Event Shop operations with blind methods. + """ + pt_preserve = 0 + + def ur_ship_costs(ships, buy=0): + total_price = sum([ship.price for ship in ships]) + if buy == 1: + if total_price == 500: + total_price = 200 + elif total_price != 0: + total_price = 0 + elif buy == 0: + total_price = 0 + return total_price + + def cal_pt_ur_should_buy(self, ships, pt_ur_stock=0): + total_price = self.ur_ship_costs(ships, buy=self.config.EventShop_BuyShipUR) + if self.event_remain_days > 0: + logger.info(f"Current UR pt: {self._pt_ur}, UR pt stock: {pt_ur_stock}, Total price: {total_price}") + return min(max(total_price - self._pt_ur, 0), pt_ur_stock) + + pt_ur_can_obtain = pt_ur_stock + self._pt_ur + buy_plan = self.config.EventShop_BuyShipUR + while buy_plan >= 0: + if pt_ur_can_obtain >= total_price: + return max(total_price - self._pt_ur, 0) + else: + logger.warning("Current UR pt cannot buy all wanted items.") + logger.info(f"Current UR pt: {self._pt_ur}, UR pt stock: {pt_ur_stock}, Total price: {total_price}") + logger.info("Try buying fewer things.") + buy_plan -= 1 + total_price = self.ur_ship_costs(ships, buy=buy_plan) + return 0 + + def should_buy_ship_ur(self, ships): + if self.event_remain_days > 0 or ships == []: + return False + else: + return self.config.EventShop_BuyShipUR > 0 and len(ships) == 2 + + def handle_buy_items_with_pt_ur(self, items): + """ + Handle UR ship buying. + Will postpone UR ship buying before event ends, + and preserve necessary Pts for UR ship. + Will buy UR pts and UR ships after event ends, + and also postpone buying UR Coins to the last. + + Returns: + list[EventShopItem]: items with UR ships deleted, and UR pt/coin items added to the end if necessary. + """ + if not self.event_shop_has_pt_ur: + return items + + ships = self.items_get_items(items, name="ShipUR", cost="URPt") + pt_ur_item = self.items_get_items(items, name="PtUR", cost="Pt") + coin_ur_item = self.items_get_items(items, name="Coin", cost="URPt") + + _items = [] + for item in items: + if not (item in ships or item in pt_ur_item or item in coin_ur_item): + _items.append(item) + + if pt_ur_item == []: + pt_ur_stock = 0 + pt_ur_should_buy = 0 + else: + pt_ur_item = pt_ur_item[0] + pt_ur_stock = pt_ur_item.count + pt_ur_should_buy = self.cal_pt_ur_should_buy(ships, pt_ur_stock) + + if pt_ur_should_buy > 0: + if self.event_remain_days > 0: + self.pt_preserve += pt_ur_should_buy * pt_ur_item.price + else: + self.event_shop_buy(pt_ur_item, amount=pt_ur_should_buy) + pt_ur_stock -= pt_ur_should_buy + pt_ur_item.count = pt_ur_stock + self._pt_ur = self.event_shop_get_pt_ur() + + if self.should_buy_ship_ur(ships): + for idx in range(self.config.EventShop_BuyShipUR): + self.event_shop_buy(ship[idx]) + self._pt_ur = self.event_shop_get_pt_ur() + + # If event ends and there is extra UR pt, add UR coin buys to last. + if coin_ur_item != [] and self.event_remain_days <= 0: + coin_ur_item = coin_ur_item[0] + if pt_ur_stock > 0: + _items.append(pt_ur_item) + _items.append(coin_ur_item) + + return _items + + def should_unlock_ship_ssr(self, ship): + if ship == []: + return False + else: + return ship[0].tag == "unobtained" and self.config.EventShop_UnlockShipSSR + + def handle_buy_ship_ssr_unlock(self, items): + """ + Will delete all locked ssr ship items if not unlocking any in the setting. + """ + ships = self.items_get_items(items, name="ShipSSR") + ships = [ship for ship in ships if ship.tag == "unobtained"] + + if ships == []: + return items + + _items = [] + for item in items: + if not item in ships: + _items.append(item) + + if not self.config.EventShop_UnlockShipSSR: + return _items + + if self.event_remain_days > 0: + self.pt_preserve += sum([ship.price for ship in ships]) + else: + for ship in ships: + self.event_shop_buy(ship) + self._pt = self.event_shop_get_pt() + ship.count -= 1 + if ship.count > 0: + _items = [ship] + _items + + return _items + + def run(self): + """ + Pages: + in: shop_event + """ + if not self.config.EventShop_Enable: + return False + self.event_shop_load_ensure() + self.record_event_shop_cost() + items = self.scan_all() + if not len(items): + logger.warning('Empty Event shop.') + return False + logger.hr("Event Shop buy", level=2) + self._pt = self.event_shop_get_pt() + if self.event_shop_has_pt_ur: + self._pt_ur = self.event_shop_get_pt_ur() + logger.attr("Event_remain_days", self.event_remain_days) + items = self.handle_buy_items_with_pt_ur(items) + items = self.handle_buy_ship_ssr_unlock(items) + if len(items) and items[-1].cost == "URPt": + if len(items) >= 2 and items[-2].name == "PtUR": + items = self.items_filter_in_event_shop(items[:-2]) + items[-2:] + else: + items = self.items_filter_in_event_shop(items[:-1]) + items[-1:] + else: + items = self.items_filter_in_event_shop(items) + if not len(items): + logger.warning('Nothing to buy.') + self._pt = self.event_shop_get_pt() + logger.attr("Pt_preserve", self.pt_preserve) + skip_get_pts = True + items.reverse() + while len(items): + item = items.pop() + if not skip_get_pts: + self._pt = self.event_shop_get_pt() + if item.cost == "Pt" and item.price + self.pt_preserve > self._pt\ + or item.cost == "URPt" and item.price > self._pt_ur: + if self.event_remain_days > 0: + logger.info(f"Pt: {self._pt}, Pt preserve: {self.pt_preserve}, price: {item.price}") + logger.info(f'Not enough pts to buy item: {item.name}, stop.') + break + else: + logger.info(f"Pt: {self._pt}, price: {item.price}") + logger.info(f'Not enough pts to buy item: {item.name}, skip.') + continue + skip_get_pts = not self.event_shop_buy(item) + if not skip_get_pts and item.name == "ShipSSR" and item.count > 1: + item.count -= 1 + items.append(item) + return True diff --git a/module/shop_event/ui.py b/module/shop_event/ui.py new file mode 100644 index 0000000000..ed83ee162d --- /dev/null +++ b/module/shop_event/ui.py @@ -0,0 +1,205 @@ +import re + +from datetime import datetime, timedelta + +from module.base.button import ButtonGrid +from module.base.decorator import cached_property +from module.base.timer import Timer +from module.config.utils import server_time_offset +from module.exception import GameStuckError, ScriptError +from module.logger import logger +from module.ocr.ocr import Digit, Ocr +from module.shop.assets import SHOP_CLICK_SAFE_AREA +from module.shop.ui import ShopUI +from module.shop_event.assets import * +from module.ui.scroll import Scroll +from module.ui.ui import UI + + +EVENT_SHOP_SCROLL = Scroll(EVENT_SHOP_SCROLL_AREA, color=(247, 211, 66)) + + +OCR_EVENT_SHOP_DEADLINE = Ocr(EVENT_SHOP_DEADLINE, lang='cnocr', name='OCR_EVENT_SHOP_DEADLINE', letter=(255, 247, 148), threshold=221) +OCR_EVENT_SHOP_ITEM_REMAIN = Digit(EVENT_SHOP_ITEM_REMAIN, name='OCR_EVENT_SHOP_ITEM_REMAIN', letter=(230, 227, 0), threshold=221) +OCR_EVENT_SHOP_PT_SSR = Digit(EVENT_SHOP_PT_SSR, letter=(239, 239, 239), name='OCR_EVENT_SHOP_PT') +OCR_EVENT_SHOP_PT_SSR_ENSURE = Ocr(EVENT_SHOP_PT_SSR, letter=(239, 239, 239), name='OCR_EVENT_SHOP_PT_SSR_ENSURE') +OCR_EVENT_SHOP_PT_UR = Digit(EVENT_SHOP_PT_UR, letter=(239, 239, 239), name='OCR_EVENT_SHOP_PT_UR') +OCR_EVENT_SHOP_PT_UR_ENSURE = Ocr(EVENT_SHOP_PT_UR, letter=(239, 239, 239), name='OCR_EVENT_SHOP_PT_UR_ENSURE') +OCR_EVENT_SHOP_SECOND_ENSURE = Ocr(EVENT_SHOP_SECOND_ENSURE, lang='jp', letter=(239, 239, 239), name='OCR_EVENT_SHOP_SECOND_ENSURE') + +class EventShopUI(UI): + """ + Class for Event Shop UI operations without specific item. + """ + def event_shop_load_ensure(self, skip_first_screenshot=True): + """ + Ensure that event shop has loaded, + using ocr of event pt values and pt icon at first item. + If ocr gets nothing, then the shop is not loaded. + + Args: + skip_first_screenshot (bool): + + Returns: + bool: whether loaded completely + """ + ensure_timeout = Timer(3, count=6).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + digits = OCR_EVENT_SHOP_PT_SSR_ENSURE.ocr(self.device.image) + # End + if digits != "": + break + else: + logger.warning("EventShop is not fully loaded, retrying.") + + # Exception + if ensure_timeout.reached(): + raise GameStuckError('Waiting too long for EventShop to appear.') + + ensure_timeout.reset() + while 1: + self.device.screenshot() + + if self.appear(EVENT_SHOP_LOAD_ENSURE): + logger.warning("EventShop is not fully loaded, retrying.") + else: + break + + if ensure_timeout.reached(): + raise GameStuckError('Waiting too long for EventShop to appear.') + + @cached_property + def event_shop_deadline(self): + """ + Returns: + datetime: server time of shop deadline. + """ + period = OCR_EVENT_SHOP_DEADLINE.ocr(self.device.image)[:-8] + y, m, d = [int(i) for i in re.split('[\~\-.]', period)[3:6]] + deadline = datetime(y, m, d) + timedelta(days=1) # server deadline + return deadline + + @cached_property + def event_remain_days(self): + if self.config.EVENT_SHOP_IGNORE_DEADLINE: + return 0 + server_now = datetime.now() - server_time_offset() + return (self.event_shop_deadline - server_now).days - 6 + + @cached_property + def event_shop_has_pt_ur(self): + """ + Event pts are aligned as follows: + - SSR event: nothing, pt + - UR event: pt, pt_ur + - assets: EVENT_SHOP_PT_UR, EVENT_SHOP_PT + Therefore we detect pt_ur by scanning the `nothing` part. + For SSR event Ocr() should get nothing, + while Digit() will always return 0. + """ + digits = OCR_EVENT_SHOP_PT_UR_ENSURE.ocr(self.device.image) + return digits != "" + + def event_shop_get_pt(self): + """ + Event pts are aligned as follows: + - SSR event: nothing, pt + - UR event: pt, pt_ur + - assets: EVENT_SHOP_PT_UR, EVENT_SHOP_PT + + Returns: + pt (int): + """ + if self.event_shop_has_pt_ur: + amount = OCR_EVENT_SHOP_PT_UR.ocr(self.device.image) + else: + amount = OCR_EVENT_SHOP_PT_SSR.ocr(self.device.image) + logger.attr("Pt", amount) + return amount + + def event_shop_get_pt_ur(self): + """ + Event pts are aligned as follows: + - SSR event: nothing, pt + - UR event: pt, pt_ur + - assets: EVENT_SHOP_PT_UR, EVENT_SHOP_PT + + Returns: + pt_ur (int): + + Raises: + ScriptError: if wrongly scans pt_ur when there isn't one. + """ + if self.event_shop_has_pt_ur: + amount = OCR_EVENT_SHOP_PT_SSR.ocr(self.device.image) + else: + raise ScriptError('Should not scan UR pt at this shop') + logger.attr("URPt", amount) + return amount + + def init_slider(self): + """Initialize the slider + + Returns: + Tuple[float, float]: (pre_pos, cur_pos) + """ + if not EVENT_SHOP_SCROLL.appear(main=self): + logger.warning('Scroll does not appear, try to rescue slider') + self.rescue_slider() + retry = Timer(0, count=3) + retry.start() + while not EVENT_SHOP_SCROLL.at_top(main=self): + logger.info('Scroll does not at top, try to scroll') + EVENT_SHOP_SCROLL.set_top(main=self) + if retry.reached(): + raise GameStuckError('Scroll drag page error.') + return -1.0, 0.0 + + def rescue_slider(self, distance=200): + detection_area = (1130, 230, 1170, 710) + direction_vector = (0, distance) + p1, p2 = random_rectangle_vector( + direction_vector, box=detection_area, random_range=(-10, -40, 10, 40), padding=10) + self.device.drag(p1, p2, segments=2, shake=(25, 0), point_random=(0, 0, 0, 0), shake_random=(-5, 0, 5, 0)) + self.device.click(SHOP_CLICK_SAFE_AREA) + self.device.screenshot() + + def pre_scroll(self, pre_pos, cur_pos): + """ + Pretreatment Sliding. + A imitation of OSShopUI.pre_scroll(). + + Args: + pre_pos: Previous position + cur_pos: Current position + + Raise: + ScriptError: Slide Page Error + + Returns: + cur_pos: Current position + """ + if pre_pos == cur_pos: + logger.warning('Scroll drag page failed') + if not EVENT_SHOP_SCROLL.appear(main=self): + logger.warning('Scroll does not appear, try to rescue slider') + self.rescue_slider() + EVENT_SHOP_SCROLL.set(cur_pos, main=self) + retry = Timer(0, count=3) + retry.start() + while 1: + logger.warning('Scroll does not drag success, retrying scroll') + EVENT_SHOP_SCROLL.next_page(main=self, page=0.66) + cur_pos = EVENT_SHOP_SCROLL.cal_position(main=self) + if pre_pos != cur_pos: + logger.info(f'Scroll success drag page to {cur_pos}') + return cur_pos + if retry.reached(): + raise GameStuckError('Scroll drag page error.') + else: + return cur_pos diff --git a/module/ui/assets.py b/module/ui/assets.py index 5b02726e0f..ab997b1bc0 100644 --- a/module/ui/assets.py +++ b/module/ui/assets.py @@ -78,7 +78,7 @@ REWARD_GOTO_TACTICAL = Button(area={'cn': (418, 413, 468, 434), 'en': (407, 416, 499, 433), 'jp': (431, 416, 476, 437), 'tw': (418, 413, 468, 435)}, color={'cn': (143, 176, 216), 'en': (157, 185, 219), 'jp': (151, 182, 217), 'tw': (141, 175, 215)}, button={'cn': (383, 404, 503, 444), 'en': (393, 404, 514, 445), 'jp': (393, 404, 514, 445), 'tw': (383, 404, 503, 444)}, file={'cn': './assets/cn/ui/REWARD_GOTO_TACTICAL.png', 'en': './assets/en/ui/REWARD_GOTO_TACTICAL.png', 'jp': './assets/jp/ui/REWARD_GOTO_TACTICAL.png', 'tw': './assets/tw/ui/REWARD_GOTO_TACTICAL.png'}) SHIPYARD_CHECK = Button(area={'cn': (9, 131, 82, 148), 'en': (4, 126, 52, 141), 'jp': (8, 130, 82, 148), 'tw': (7, 130, 83, 150)}, color={'cn': (159, 180, 229), 'en': (133, 148, 171), 'jp': (152, 169, 202), 'tw': (142, 164, 219)}, button={'cn': (9, 131, 82, 148), 'en': (4, 126, 52, 141), 'jp': (8, 130, 82, 148), 'tw': (7, 130, 83, 150)}, file={'cn': './assets/cn/ui/SHIPYARD_CHECK.png', 'en': './assets/en/ui/SHIPYARD_CHECK.png', 'jp': './assets/jp/ui/SHIPYARD_CHECK.png', 'tw': './assets/tw/ui/SHIPYARD_CHECK.png'}) SHOP_CHECK = Button(area={'cn': (93, 504, 134, 544), 'en': (102, 512, 172, 548), 'jp': (93, 504, 134, 544), 'tw': (92, 504, 133, 545)}, color={'cn': (194, 204, 222), 'en': (159, 178, 205), 'jp': (185, 196, 217), 'tw': (184, 199, 221)}, button={'cn': (93, 504, 134, 544), 'en': (102, 512, 172, 548), 'jp': (93, 504, 134, 544), 'tw': (92, 504, 133, 545)}, file={'cn': './assets/cn/ui/SHOP_CHECK.png', 'en': './assets/en/ui/SHOP_CHECK.png', 'jp': './assets/jp/ui/SHOP_CHECK.png', 'tw': './assets/tw/ui/SHOP_CHECK.png'}) -SHOP_GOTO_MUNITIONS = Button(area={'cn': (840, 560, 999, 600), 'en': (840, 560, 999, 600), 'jp': (836, 570, 964, 613), 'tw': (840, 560, 999, 600)}, color={'cn': (122, 87, 75), 'en': (122, 87, 75), 'jp': (118, 88, 83), 'tw': (122, 87, 75)}, button={'cn': (840, 560, 999, 600), 'en': (840, 560, 999, 600), 'jp': (836, 570, 964, 613), 'tw': (840, 560, 999, 600)}, file={'cn': './assets/cn/ui/SHOP_GOTO_MUNITIONS.png', 'en': './assets/en/ui/SHOP_GOTO_MUNITIONS.png', 'jp': './assets/jp/ui/SHOP_GOTO_MUNITIONS.png', 'tw': './assets/tw/ui/SHOP_GOTO_MUNITIONS.png'}) +SHOP_GOTO_MUNITIONS = Button(area={'cn': (1068, 564, 1158, 629), 'en': (1068, 564, 1158, 629), 'jp': (1068, 564, 1158, 629), 'tw': (1068, 564, 1158, 629)}, color={'cn': (94, 77, 83), 'en': (94, 77, 83), 'jp': (94, 77, 83), 'tw': (94, 77, 83)}, button={'cn': (1068, 564, 1158, 629), 'en': (1068, 564, 1158, 629), 'jp': (1068, 564, 1158, 629), 'tw': (1068, 564, 1158, 629)}, file={'cn': './assets/cn/ui/SHOP_GOTO_MUNITIONS.png', 'en': './assets/en/ui/SHOP_GOTO_MUNITIONS.png', 'jp': './assets/jp/ui/SHOP_GOTO_MUNITIONS.png', 'tw': './assets/tw/ui/SHOP_GOTO_MUNITIONS.png'}) SHOP_GOTO_SUPPLY_PACK = Button(area={'cn': (883, 537, 929, 579), 'en': (883, 537, 929, 579), 'jp': (883, 537, 929, 579), 'tw': (883, 537, 929, 579)}, color={'cn': (197, 131, 140), 'en': (197, 131, 140), 'jp': (197, 131, 140), 'tw': (197, 131, 140)}, button={'cn': (810, 353, 987, 638), 'en': (810, 353, 987, 638), 'jp': (810, 353, 987, 638), 'tw': (810, 353, 987, 638)}, file={'cn': './assets/cn/ui/SHOP_GOTO_SUPPLY_PACK.png', 'en': './assets/en/ui/SHOP_GOTO_SUPPLY_PACK.png', 'jp': './assets/jp/ui/SHOP_GOTO_SUPPLY_PACK.png', 'tw': './assets/tw/ui/SHOP_GOTO_SUPPLY_PACK.png'}) SP_CHECK = Button(area={'cn': (123, 63, 206, 109), 'en': (123, 63, 206, 109), 'jp': (125, 66, 205, 107), 'tw': (123, 63, 206, 109)}, color={'cn': (95, 110, 145), 'en': (95, 110, 145), 'jp': (78, 92, 127), 'tw': (95, 110, 145)}, button={'cn': (123, 63, 206, 109), 'en': (123, 63, 206, 109), 'jp': (125, 66, 205, 107), 'tw': (123, 63, 206, 109)}, file={'cn': './assets/cn/ui/SP_CHECK.png', 'en': './assets/en/ui/SP_CHECK.png', 'jp': './assets/jp/ui/SP_CHECK.png', 'tw': './assets/tw/ui/SP_CHECK.png'}) STORAGE_CHECK = Button(area={'cn': (120, 14, 175, 40), 'en': (123, 14, 209, 36), 'jp': (121, 13, 176, 41), 'tw': (120, 14, 175, 40)}, color={'cn': (137, 151, 188), 'en': (119, 133, 174), 'jp': (137, 156, 196), 'tw': (137, 151, 188)}, button={'cn': (120, 14, 175, 40), 'en': (123, 14, 209, 36), 'jp': (121, 13, 176, 41), 'tw': (120, 14, 175, 40)}, file={'cn': './assets/cn/ui/STORAGE_CHECK.png', 'en': './assets/en/ui/STORAGE_CHECK.png', 'jp': './assets/jp/ui/STORAGE_CHECK.png', 'tw': './assets/cn/ui/STORAGE_CHECK.png'})