diff --git a/assets/shop/os/ActionPoint100_1.png b/assets/shop/os/ActionPoint100_1.png new file mode 100644 index 000000000..579032130 Binary files /dev/null and b/assets/shop/os/ActionPoint100_1.png differ diff --git a/assets/shop/os/GearDesign PlanT2.png b/assets/shop/os/GearDesignPlanT2.png similarity index 100% rename from assets/shop/os/GearDesign PlanT2.png rename to assets/shop/os/GearDesignPlanT2.png diff --git a/assets/shop/os/GearDesign PlanT3.png b/assets/shop/os/GearDesignPlanT3.png similarity index 100% rename from assets/shop/os/GearDesign PlanT3.png rename to assets/shop/os/GearDesignPlanT3.png diff --git a/assets/shop/os/GearPartSpecialized_1.png b/assets/shop/os/GearPartSpecialized_1.png new file mode 100644 index 000000000..1c88846a2 Binary files /dev/null and b/assets/shop/os/GearPartSpecialized_1.png differ diff --git a/assets/shop/os/LoggerAbyssalT4_2.png b/assets/shop/os/LoggerAbyssalT4_2.png new file mode 100644 index 000000000..cff4addae Binary files /dev/null and b/assets/shop/os/LoggerAbyssalT4_2.png differ diff --git a/assets/shop/os/METARedBookT2_1.png b/assets/shop/os/METARedBookT2_1.png new file mode 100644 index 000000000..7e31f596b Binary files /dev/null and b/assets/shop/os/METARedBookT2_1.png differ diff --git a/assets/shop/os/PlateRandomT4_1.png b/assets/shop/os/PlateRandomT4_1.png new file mode 100644 index 000000000..26141b95e Binary files /dev/null and b/assets/shop/os/PlateRandomT4_1.png differ diff --git a/assets/shop/os/PurpleCoins_1.png b/assets/shop/os/PurpleCoins_1.png new file mode 100644 index 000000000..1694f3dfa Binary files /dev/null and b/assets/shop/os/PurpleCoins_1.png differ diff --git a/assets/shop/os/RepairPackFull2_1.png b/assets/shop/os/RepairPackFull2_1.png new file mode 100644 index 000000000..885e75a38 Binary files /dev/null and b/assets/shop/os/RepairPackFull2_1.png differ diff --git a/assets/shop/os/RepairPackFull_1.png b/assets/shop/os/RepairPackFull_1.png new file mode 100644 index 000000000..61839f2b4 Binary files /dev/null and b/assets/shop/os/RepairPackFull_1.png differ diff --git a/assets/shop/os/TuningCombatT2_1.png b/assets/shop/os/TuningCombatT2_1.png new file mode 100644 index 000000000..a4c2a1f6f Binary files /dev/null and b/assets/shop/os/TuningCombatT2_1.png differ diff --git a/config/template.json b/config/template.json index 71c288492..8f9bbbbda 100644 --- a/config/template.json +++ b/config/template.json @@ -1617,7 +1617,9 @@ "ServerUpdate": "00:00" }, "OpsiShop": { - "BuySupply": true + "BuySupply": true, + "PresetFilter": "max_benefit_meta", + "CustomFilter": "Logger" }, "Storage": { "Storage": {} diff --git a/module/base/filter.py b/module/base/filter.py index a75dada67..0c12e7234 100644 --- a/module/base/filter.py +++ b/module/base/filter.py @@ -45,7 +45,7 @@ class Filter: """ Args: objs (list): List of objects and strings - func (callable): A function to filter object. + List[func]|func (callable): A function or a list of funciton that to filter object. Function should receive an object as arguments, and return a bool. True means add it to output. @@ -68,6 +68,9 @@ class Filter: for obj in objs: if isinstance(obj, str): out.append(obj) + elif isinstance(func, list): + if all(f(obj) for f in func): + out.append(obj) elif func(obj): out.append(obj) else: diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 90f2852b7..ab351f471 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -8076,6 +8076,21 @@ "BuySupply": { "type": "checkbox", "value": true + }, + "PresetFilter": { + "type": "select", + "value": "max_benefit_meta", + "option": [ + "max_benefit", + "max_benefit_meta", + "no_meta", + "all", + "custom" + ] + }, + "CustomFilter": { + "type": "textarea", + "value": "Logger" } }, "Storage": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 83071826c..b980e60db 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -622,6 +622,16 @@ OpsiExplore: LastZone: 0 OpsiShop: BuySupply: true + PresetFilter: + value: max_benefit_meta + option: + - max_benefit + - max_benefit_meta + - no_meta + - all + - custom + CustomFilter: |- + Logger OpsiVoucher: Filter: |- LoggerAbyssal > LoggerObscure > Book > Coin > Fragment diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 1feaa08a1..b4d190429 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -381,6 +381,8 @@ class GeneratedConfig: # Group `OpsiShop` OpsiShop_BuySupply = True + OpsiShop_PresetFilter = 'max_benefit_meta' # max_benefit, max_benefit_meta, no_meta, all, custom + OpsiShop_CustomFilter = 'Logger' # Group `OpsiVoucher` OpsiVoucher_Filter = 'LoggerAbyssal > LoggerObscure > Book > Coin > Fragment' diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 6f6dd4ab1..bf3f02a50 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -2215,6 +2215,19 @@ "BuySupply": { "name": "Buy From Port Shops", "help": "Buy all items from port shops\nShop inventory consists of a fixed pool that is reset monthly, items not bought during a cycle has the chance of re-appearing and blocking preferable high value items" + }, + "PresetFilter": { + "name": "OpSi Shop Filter", + "help": "Generally does not need to be modified. Use \"High value items and META material\"` if Voucher Coins spilled or \"Without META materials\" if you are newcomer.\nHigh value items include ActionPoint, Logger and T4 or higher items; META materials include METABook and META enhance materials.", + "max_benefit": "High value items", + "max_benefit_meta": "High value items and META materials", + "no_meta": "Not to buy META materials", + "all": "All", + "custom": "custom" + }, + "CustomFilter": { + "name": "Custom Research Priority", + "help": "To use your own filter, set \"OpSi Shop Filter Select\" to \"custom\". All options have been defined at " } }, "OpsiVoucher": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index f67808b98..4c25f3f1c 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -2215,6 +2215,19 @@ "BuySupply": { "name": "OpsiShop.BuySupply.name", "help": "OpsiShop.BuySupply.help" + }, + "PresetFilter": { + "name": "OpsiShop.PresetFilter.name", + "help": "OpsiShop.PresetFilter.help", + "max_benefit": "max_benefit", + "max_benefit_meta": "max_benefit_meta", + "no_meta": "no_meta", + "all": "all", + "custom": "custom" + }, + "CustomFilter": { + "name": "OpsiShop.CustomFilter.name", + "help": "OpsiShop.CustomFilter.help" } }, "OpsiVoucher": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 447f73d28..16de0019e 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -2215,6 +2215,19 @@ "BuySupply": { "name": "购买港口商店", "help": "每月港口商店可购买商品是固定的,未购买的物品下次仍会出现,并阻塞高价值物品,因此需要购买全部" + }, + "PresetFilter": { + "name": "港口商店过滤器", + "help": "一般默认即可,白票溢出建议选 \"高价值物品与META材料\",萌新建议选 \"不购买META材料\",这会买除了META材料以外的所有东西。\n高价值物品为行动力、坐标和金或者更高级別的商品;META材料为META通用战术教材和4种强化材料。", + "max_benefit": "高价值物品", + "max_benefit_meta": "高价值物品与META材料", + "no_meta": "不购买META材料", + "all": "全部", + "custom": "自定义" + }, + "CustomFilter": { + "name": "自定义过滤器", + "help": "使用自定义过滤器需将 \"港口商店过滤器\" 设置为 \"自定义\",并阅读 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_cn" } }, "OpsiVoucher": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index f2f112709..50ba77212 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -2215,6 +2215,19 @@ "BuySupply": { "name": "購買港口商店", "help": "每月港口商店可購買商品是固定的,未購買的物品下次仍會出現,並阻擋高價值物品,因此需要購買全部" + }, + "PresetFilter": { + "name": "港口商店過濾器", + "help": "一般用預設即可,白票溢出建議選 \"高價值物品與META材料\",新手建議選 \"不購買META材料\",這會買除了META材料以外的所有東西。\n高價值物品為行動點、坐標和金或者更高級別的物品;META材料為META通用戰術教材和4種強化材料。", + "max_benefit": "高價值物品", + "max_benefit_meta": "高價值物品與META材料", + "no_meta": "不購買META材料", + "all": "全部", + "custom": "自定義過濾器" + }, + "CustomFilter": { + "name": "自定義過濾器", + "help": "使用自定義過濾器需將 \"港口商店過濾器\" 設定為 \"自定義\",並閱讀 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_cn" } }, "OpsiVoucher": { diff --git a/module/os_handler/preset.py b/module/os_handler/preset.py new file mode 100644 index 000000000..2e710d481 --- /dev/null +++ b/module/os_handler/preset.py @@ -0,0 +1,18 @@ +OS_SHOP = { + 'max_benefit': """ + ActionPoint > DevelopmentMaterialT3 > GearDesignPlan > GearPart > Logger > OrdnanceTestingReport > PlateRandom + """, + 'max_benefit_meta': """ + ActionPoint > DevelopmentMaterialT3 > GearDesignPlan > GearPart > Logger > OrdnanceTestingReport > PlateRandom > + METARedBook > CrystallizedHeatResistantSteel > NanoceramicAlloy > NeuroplasticProstheticArm > SupercavitationGenerator + """, + 'no_meta':""" + ActionPoint > DevelopmentMaterial > EnergyStorageDevice > GearDesignPlan > GearPart > + Logger > PurpleCoins > PlateRandom > RepairPack > Turing > TuringSample + """, + 'all': """ + ActionPoint > CrystallizedHeatResistantSteel > DevelopmentMaterial > EnergyStorageDevice > GearDesignPlan > GearPart > + Logger > METARedBook > NanoceramicAlloy > NeuroplasticProstheticArm > OrdnanceTestingReport > PurpleCoins > PlateRandom > + RepairPack > SupercavitationGenerator > Turing > TuringSample + """ +} diff --git a/module/os_handler/selector.py b/module/os_handler/selector.py new file mode 100644 index 000000000..dbe5ce6ca --- /dev/null +++ b/module/os_handler/selector.py @@ -0,0 +1,124 @@ +import re +from module.config.config_generated import GeneratedConfig + +from module.os_handler.preset import * +from module.base.filter import Filter + +FILTER_REGEX = re.compile( + '^(actionpoint|crystallizedheatresistantsteel|developmentmaterial' + '|energystoragedevice|geardesignplan|gearpart|logger|metaredbook' + '|nanoceramicalloy|neuroplasticprostheticarm|ordnancetestingreport' + '|platerandom|purplecoins|repairpack|supercavitationgenerator|turingsample' + '|turings)' + + '(20|50|100|prototype|specialized|abyssal|obscure|full2|full|triple2|triple|2' + '|combat|offence|survival)?' + + '(t[1-6])?$', + flags=re.IGNORECASE) +FILTER_ATTR = ('group', 'sub_genre', 'tier') +FILTER = Filter(FILTER_REGEX, FILTER_ATTR) + + +class Selector(): + prise_yellow_coin = 0 + prise_purple_coin = 0 + + def clear_prise(self): + self.prise_yellow_coin = 0 + self.prise_purple_coin = 0 + + def pretreatment(self, items) -> list: + """ + Pretreatment items. + + Args: + items: + + Returns: + list[Item]: + """ + _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 enough_coins(self, item) -> bool: + """ + Check if there are enough coins to buy the item. + + Args: + item: + + Returns: + bool: True if there are enough coins. + """ + if item.cost == 'YellowCoins' and self.prise_yellow_coin + item.price <= \ + self._shop_yellow_coins - (self.config.OS_CL1_YELLOW_COINS_PRESERVE if self.is_cl1_enabled else 0): + self.prise_yellow_coin += item.price + return True + if item.cost == 'PurpleCoins' and self.prise_purple_coin + item.price <= self._shop_purple_coins: + self.prise_purple_coin += item.price + return True + + return False + + def check_cl1_purple_coins(self, item) -> bool: + """ + Check if cl1 is enable and item name is PurpleCoins. + + Args: + item: + + Returns: + bool: False if cl1 is enable and item name is PurpleCoins. + """ + return not (self.is_cl1_enabled and item.name == 'PurpleCoins') + + def items_filter_in_akashi_shop(self, items) -> list: + """ + Returns items that can be bought. + + Args: + items: Irems to be filtered. + + Returns: + list[Item]: + """ + self.clear_prise() + items = self.pretreatment(items) + parser = self.config.OpsiGeneral_AkashiShopFilter + if not parser.strip(): + parser = GeneratedConfig.OpsiGeneral_AkashiShopFilter + FILTER.load(parser) + return FILTER.apply(items, func=self.enough_coins) + + def items_filter_in_os_shop(self, items) -> list: + """ + Returns items that can be bought. + + Args: + items: Items to be filtered. + + Returns: + list[Item]: + """ + self.clear_prise() + items = self.pretreatment(items) + preset = self.config.OpsiShop_PresetFilter + parser = '' + if preset == 'custom': + parser = self.config.OpsiShop_CustomFilter + if not parser.strip(): + parser = OS_SHOP[GeneratedConfig.OpsiShop_PresetFilter] + else: + parser = OS_SHOP[preset] + FILTER.load(parser) + return FILTER.apply(items, func=[self.enough_coins, self.check_cl1_purple_coins]) diff --git a/module/os_handler/shop.py b/module/os_handler/shop.py index 9269b9989..bdc06bcab 100644 --- a/module/os_handler/shop.py +++ b/module/os_handler/shop.py @@ -8,8 +8,9 @@ from module.os_handler.assets import * from module.os_handler.map_event import MapEventHandler from module.os_handler.os_status import OSStatus from module.os_handler.ui import OSShopUI -from module.statistics.item import ItemGrid +from module.os_handler.selector import Selector from module.base.decorator import Config +from module.statistics.item import ItemGrid from module.ui.scroll import Scroll from module.shop.assets import AMOUNT_MAX, AMOUNT_MINUS, AMOUNT_PLUS, SHOP_BUY_CONFIRM_AMOUNT, SHOP_BUY_CONFIRM as OS_SHOP_BUY_CONFIRM from module.shop.clerk import OCR_SHOP_AMOUNT @@ -37,7 +38,7 @@ class OSShopPrice(DigitYuv): return result -class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): +class OSShopHandler(OSStatus, OSShopUI, Selector, MapEventHandler): _shop_yellow_coins = 0 _shop_purple_coins = 0 @@ -46,6 +47,13 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): self._shop_purple_coins = self.get_purple_coins() logger.info(f'Yellow coins: {self._shop_yellow_coins}, purple coins: {self._shop_purple_coins}') + @Config.when(SERVER='tw') + def os_shop_get_coins_in_os_shop(self): + self._shop_yellow_coins = self.get_yellow_coins() + self._shop_purple_coins = self.get_purple_coins() + logger.info(f'Yellow coins: {self._shop_yellow_coins}, purple coins: {self._shop_purple_coins}') + + @Config.when(SERVER=None) def os_shop_get_coins_in_os_shop(self): self._shop_yellow_coins = self.get_yellow_coins() self._shop_purple_coins = self.get_purple_coins_in_os_shop() @@ -53,7 +61,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): @cached_property @Config.when(SERVER='tw') - def os_shop_items(self): + def os_shop_items(self) -> ItemGrid: """ Returns: ItemGrid: @@ -69,7 +77,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): @cached_property @Config.when(SERVER='en') - def os_shop_items(self): + def os_shop_items(self) -> ItemGrid: """ Returns: ItemGrid: @@ -85,7 +93,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): @cached_property @Config.when(SERVER=None) - def os_shop_items(self): + def os_shop_items(self) -> ItemGrid: """ Returns: ItemGrid: @@ -207,55 +215,27 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): """ costs = self._get_os_shop_cost() items = [] - for cost in costs: shop_items = self._get_os_shop_items(cost) + if self.config.SHOP_EXTRACT_TEMPLATE: + shop_items.extract_template(self.device.image, './assets/shop/os') shop_items.predict(self.device.image, name=name, amount=name, cost=True, price=True) shop_items = shop_items.items if len(shop_items): row = [str(item) for item in shop_items] logger.info(f'Shop items found: {row}') - items += self.items_filter_in_os_shop(shop_items) + items += shop_items else: logger.info('No shop items found') return items - def items_filter_in_os_shop(self, items) -> list: + def os_shop_get_item_to_buy_in_akashi(self) -> list: """ - Returns items that can be bought. - - Args: - items: Items to be filtered. - Returns: list[Item]: """ - _items = [] - prise_yellow_coin = 0 - prise_purple_coin = 0 - - for item in items: - if self.is_cl1_enabled and item.name == 'PurpleCoins': - continue - if item.cost == 'YellowCoins' and prise_yellow_coin + item.price <= \ - self._shop_yellow_coins - (self.config.OS_CL1_YELLOW_COINS_PRESERVE if self.is_cl1_enabled else 0): - prise_yellow_coin += item.price - _items.append(item) - continue - if item.cost == 'PurpleCoins' and prise_purple_coin + item.price <= self._shop_purple_coins: - prise_purple_coin += item.price - _items.append(item) - continue - - return _items - - def os_shop_get_item_to_buy_in_akashi(self): - """ - Returns: - Item: - """ self.os_shop_get_coins() items = self.os_shop_get_items_in_akashi(name=True) # Shop supplies do not appear immediately, need to confirm if shop is empty. @@ -269,40 +249,8 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): else: break - try: - selection = self.config.OpsiGeneral_AkashiShopFilter.replace(' ', '').replace('\n', '').split('>') - except Exception: - logger.warning(f'Invalid OS akashi buy filter string: {self.config.OpsiGeneral_AkashiShopFilter}') - return None + return self.items_filter_in_os_shop(items) - # TODO: Better filter for Akashi shop. - for select in selection: - for item in items: - if select not in item.name: - continue - if item.cost == 'YellowCoins': - if item.price > self._shop_yellow_coins: - continue - if item.cost == 'PurpleCoins': - if item.price > self._shop_purple_coins: - continue - - return item - - return None - - @Config.when(SERVER='tw') - def os_shop_get_item_to_buy_in_port(self): - """ - Returns: - Item: - """ - self.os_shop_get_coins() - logger.attr('CL1 enabled', self.is_cl1_enabled) - - return self.os_shop_get_items(name=True) - - @Config.when(SERVER=None) def os_shop_get_item_to_buy_in_port(self) -> list: """ Returns: @@ -320,12 +268,12 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): items = self.os_shop_get_items(name=True) continue else: - self.os_shop_items.items = items - return items + self.os_shop_items.items = self.items_filter_in_os_shop(items) + return self.os_shop_items.items return [] - def os_shop_buy_execute(self, button, skip_first_screenshot=True): + def os_shop_buy_execute(self, button, skip_first_screenshot=True) -> bool: """ Args: button: Item to buy @@ -378,32 +326,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): return success - def os_shop_buy(self, select_func): - """ - Args: - select_func: - - Returns: - int: Items bought. - - Pages: - in: PORT_SUPPLY_CHECK - """ - count = 0 - for _ in range(12): - button = select_func() - if button is None: - logger.info('Shop buy finished') - return count - else: - self.os_shop_buy_execute(button) - count += 1 - continue - - logger.warning('Too many items to buy, stopped') - return count - - def os_shop_buy_2(self, select_func) -> int: + def os_shop_buy(self, select_func) -> int: """ Args: select_func: @@ -422,7 +345,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): logger.info('No items need to be purchased') continue for button in buttons: - if count >= 5: + if count >= 10: logger.info('Shop buy finished') return count else: @@ -453,7 +376,7 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): if item.cost == 'YellowCoins' else self._shop_purple_coins if (_currency < item.price): return False - + if self.appear(AMOUNT_MAX, offset=(50, 50)): limit = None for _ in range(3): @@ -472,11 +395,11 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): diff = limit - total if diff > 0: limit = total - self.ui_ensure_index(limit, letter=OCR_SHOP_AMOUNT, prev_button=AMOUNT_MINUS, next_button=AMOUNT_PLUS, - skip_first_screenshot=True) - - self.device.click(SHOP_BUY_CONFIRM_AMOUNT) + skip_first_screenshot=True) + + self.appear_then_click(OS_SHOP_BUY_CONFIRM, offset=(20, 20)) + self.appear_then_click(SHOP_BUY_CONFIRM_AMOUNT, offset=(20, 20)) return True @@ -503,23 +426,25 @@ class OSShopHandler(OSStatus, OSShopUI, MapEventHandler): Pages: in: PORT_SUPPLY_CHECK """ + _count = 0 for i in range(4): count = 0 - self.os_shop_side_navbar_ensure(upper=i + 1) - OS_SHOP_SCROLL.set_top(main=self) + self.os_shop_side_navbar_ensure(bottom=i + 1) + OS_SHOP_SCROLL.set_bottom(main=self) while True: - count += self.os_shop_buy_2(select_func=self.os_shop_get_item_to_buy_in_port) - if count >= 5: + count += self.os_shop_buy(select_func=self.os_shop_get_item_to_buy_in_port) + if count >= 10: break - elif OS_SHOP_SCROLL.at_bottom(main=self): + elif OS_SHOP_SCROLL.at_top(main=self): logger.info('OS shop reach bottom, stop') break else: - OS_SHOP_SCROLL.next_page(main=self, page=0.66) + OS_SHOP_SCROLL.prev_page(main=self, page=0.66) continue + _count += count - return count > 0 or len(self.os_shop_items.items) == 0 + return _count > 0 or len(self.os_shop_items.items) == 0 def handle_akashi_supply_buy(self, grid): """