diff --git a/assets/reward/TACTICAL_CLASS_START.png b/assets/reward/TACTICAL_CLASS_START.png new file mode 100644 index 000000000..0f1a0ff6f Binary files /dev/null and b/assets/reward/TACTICAL_CLASS_START.png differ diff --git a/assets/ui/REWARD_GOTO_TACTICAL.png b/assets/ui/REWARD_GOTO_TACTICAL.png new file mode 100644 index 000000000..e3297a994 Binary files /dev/null and b/assets/ui/REWARD_GOTO_TACTICAL.png differ diff --git a/assets/ui/TACTICAL_CHECK.png b/assets/ui/TACTICAL_CHECK.png new file mode 100644 index 000000000..53d3e2663 Binary files /dev/null and b/assets/ui/TACTICAL_CHECK.png differ diff --git a/config/template.ini b/config/template.ini index 6ae613d54..2c65aab20 100644 --- a/config/template.ini +++ b/config/template.ini @@ -60,6 +60,7 @@ enable_oil_reward = yes enable_coin_reward = yes enable_mission_reward = yes enable_commission_reward = yes +enable_tactical_reward = yes commission_time_limit = 23:30 duration_shorter_than_2 = 11 duration_longer_than_6 = -11 @@ -79,6 +80,11 @@ urgent_book = 95 urgent_box = 195 urgent_gem = 205 urgent_ship = 155 +tactical_night_range = 23:30-06:30 +tactical_book_tier = 2 +tactical_exp_first = yes +tactical_book_tier_night = 3 +tactical_exp_first_night = no [Emulator] command = emulator diff --git a/module/base/timer.py b/module/base/timer.py index 93da5c310..a6dfe9dea 100644 --- a/module/base/timer.py +++ b/module/base/timer.py @@ -23,7 +23,7 @@ def future_time(string): string (str): Such as 14:59. Returns: - datetime: Time with given hour, minute, second in the future. + datetime.datetime: Time with given hour, minute, second in the future. """ hour, minute = [int(x) for x in string.split(':')] future = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0) @@ -31,6 +31,31 @@ def future_time(string): return future +def future_time_range(string): + """ + Args: + string (str): Such as 23:30-06:30. + + Returns: + tuple(datetime.datetime): (time start, time end). + """ + start, end = [future_time(s) for s in string.split('-')] + if start > end: + start = start - timedelta(days=1) + return start, end + + +def time_range_active(time_range): + """ + Args: + time_range(tuple(datetime.datetime)): (time start, time end). + + Returns: + bool: + """ + return time_range[0] < datetime.now() < time_range[1] + + class Timer: def __init__(self, limit, count=0): """ @@ -43,8 +68,8 @@ class Timer: if self.appear(MAIN_CHECK): if confirm_timer.reached(): pass - else: - confirm_timer.reset() + else: + confirm_timer.reset() """ self.limit = limit self.count = count diff --git a/module/config/argparser.py b/module/config/argparser.py index aa5e9cd58..c747b38b8 100644 --- a/module/config/argparser.py +++ b/module/config/argparser.py @@ -250,6 +250,14 @@ def main(ini_name=''): priority4.add_argument('--钻石类紧急委托', default=default('--钻石类紧急委托'), help='BIW要员护卫, NYB巡视护卫') priority4.add_argument('--观舰类紧急委托', default=default('--观舰类紧急委托'), help='小型观舰仪式, 同盟观舰仪式') + reward_tactical = reward_parser.add_argument_group('战术学院', '只支持续技能书, 不支持学新技能') + reward_tactical.add_argument('--启用战术学院收获', default=default('--启用战术学院收获'), choices=['是', '否']) + reward_tactical.add_argument('--战术学院夜间时段', default=default('--战术学院夜间时段'), help='格式 23:30-06:30') + reward_tactical.add_argument('--技能书稀有度', default=default('--技能书稀有度'), choices=['3', '2', '1'], help='最多使用T几的技能书\nT3是金书, T2是紫书, T1是蓝书') + reward_tactical.add_argument('--技能书优先使用同类型', default=default('--技能书优先使用同类型'), choices=['是', '否'], help='选是, 优先使用有150%加成的\n选否, 优先使用同稀有度的技能书') + reward_tactical.add_argument('--技能书夜间稀有度', default=default('--技能书夜间稀有度'), choices=['3', '2', '1']) + reward_tactical.add_argument('--技能书夜间优先使用同类型', default=default('--技能书夜间优先使用同类型'), choices=['是', '否']) + # ==========设备设置========== emulator_parser = subs.add_parser('设备设置') emulator = emulator_parser.add_argument_group('模拟器', '需要运行一次来保存选项, 会检查游戏是否启动') diff --git a/module/config/config.py b/module/config/config.py index b03d0de3e..66c94f7e6 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -2,13 +2,12 @@ import codecs import configparser import copy import os -from datetime import datetime import cv2 import numpy as np from PIL import Image -from module.base.timer import future_time +from module.base.timer import * from module.config.dictionary import * from module.logger import logger @@ -282,6 +281,8 @@ class AzurLaneConfig: ENABLE_COIN_REWARD = True ENABLE_MISSION_REWARD = True ENABLE_COMMISSION_REWARD = True + ENABLE_TACTICAL_REWARD = True + COMMISSION_PRIORITY = { 'major_comm': 0, 'daily_comm': 100, @@ -304,6 +305,12 @@ class AzurLaneConfig: } COMMISSION_TIME_LIMIT = 0 + TACTICAL_BOOK_TIER = 2 + TACTICAL_EXP_FIRST = True + TACTICAL_BOOK_TIER_NIGHT = 3 + TACTICAL_EXP_FIRST_NIGHT = False + TACTICAL_NIGHT_RANGE = future_time_range('23:30-06:30') # (Night start, night end), datetime.datetime instance. + """ C_7_2_mystery_farming """ @@ -425,11 +432,16 @@ class AzurLaneConfig: # Reward option = config['Reward'] self.REWARD_INTERVAL = int(option['reward_interval']) - for attr in ['enable_reward', 'enable_oil_reward', 'enable_coin_reward', 'enable_mission_reward', 'enable_commission_reward']: + for attr in ['enable_reward', 'enable_oil_reward', 'enable_coin_reward', 'enable_mission_reward', 'enable_commission_reward', 'enable_tactical_reward']: self.__setattr__(attr.upper(), to_bool(option[attr])) self.COMMISSION_TIME_LIMIT = future_time(option['commission_time_limit']) for attr in self.COMMISSION_PRIORITY.keys(): self.COMMISSION_PRIORITY[attr] = int(option[attr]) + self.TACTICAL_NIGHT_RANGE = future_time_range(option['tactical_night_range']) + self.TACTICAL_BOOK_TIER = int(option['tactical_book_tier']) + self.TACTICAL_EXP_FIRST = to_bool(option['tactical_exp_first']) + self.TACTICAL_BOOK_TIER_NIGHT = int(option['tactical_book_tier_night']) + self.TACTICAL_EXP_FIRST_NIGHT = to_bool(option['tactical_exp_first_night']) option = config['Main'] self.CAMPAIGN_NAME = option['main_stage'] @@ -443,12 +455,12 @@ class AzurLaneConfig: for n in [1, 2, 4, 5]: self.DAILY_CHOOSE[n] = dic_daily[option[f'daily_mission_{n}']] self.FLEET_DAILY = int(option['daily_fleet']) - self.FLEET_DAILY_EQUIPMENT = equip(option['daily_equipment']) + self.FLEET_DAILY_EQUIPMENT = to_list(option['daily_equipment']) # Hard self.ENABLE_HARD_CAMPAIGN = to_bool(option['enable_hard_campaign']) self.HARD_CAMPAIGN = option['hard_campaign'] self.FLEET_HARD = int(option['hard_fleet']) - self.FLEET_HARD_EQUIPMENT = equip(option['hard_equipment']) + self.FLEET_HARD_EQUIPMENT = to_list(option['hard_equipment']) # Exercise self.ENABLE_EXERCISE = to_bool(option['enable_exercise']) self.EXERCISE_CHOOSE_MODE = option['exercise_choose_mode'] @@ -456,7 +468,7 @@ class AzurLaneConfig: self.OPPONENT_CHALLENGE_TRIAL = int(option['exercise_try']) self.LOW_HP_THRESHOLD = float(option['exercise_hp_threshold']) self.LOW_HP_CONFIRM_WAIT = float(option['exercise_low_hp_confirm']) - self.EXERCISE_FLEET_EQUIPMENT = equip(option['exercise_equipment']) + self.EXERCISE_FLEET_EQUIPMENT = to_list(option['exercise_equipment']) # Event option = config['Event'] diff --git a/module/config/dictionary.py b/module/config/dictionary.py index ff8f8442d..2a4e49b90 100644 --- a/module/config/dictionary.py +++ b/module/config/dictionary.py @@ -91,6 +91,7 @@ dic_chi_to_eng = { '启用物资收获': 'enable_coin_reward', '启用任务收获': 'enable_mission_reward', '启用委托收获': 'enable_commission_reward', + '启用战术学院收获': 'enable_tactical_reward', '委托时间限制': 'commission_time_limit', '委托耗时小于2h': 'duration_shorter_than_2', '委托耗时超过6h': 'duration_longer_than_6', @@ -110,6 +111,11 @@ dic_chi_to_eng = { '装备类紧急委托': 'urgent_box', '钻石类紧急委托': 'urgent_gem', '观舰类紧急委托': 'urgent_ship', + '战术学院夜间时段': 'tactical_night_range', + '技能书稀有度': 'tactical_book_tier', + '技能书优先使用同类型': 'tactical_exp_first', + '技能书夜间稀有度': 'tactical_book_tier_night', + '技能书夜间优先使用同类型': 'tactical_exp_first_night', '设备': 'serial', '包名': 'package_name', '出错时保存log和截图': 'enable_error_log_and_screenshot_save', @@ -198,10 +204,8 @@ def to_bool(string): return dic_bool.get(string, string) -def equip(string): +def to_list(string): if string.isdigit(): return None - out = [int(letter.strip()) for letter in string.split(',')] - assert len(out) == 6 return out diff --git a/module/reward/assets.py b/module/reward/assets.py index 6833544fc..ac490fc21 100644 --- a/module/reward/assets.py +++ b/module/reward/assets.py @@ -21,3 +21,4 @@ REWARD_1 = Button(area=(383, 285, 503, 297), color=(238, 168, 81), button=(383, REWARD_2 = Button(area=(383, 404, 503, 444), color=(233, 165, 67), button=(383, 404, 503, 444), file='./assets/reward/REWARD_2.png') REWARD_3 = Button(area=(383, 546, 503, 586), color=(234, 163, 69), button=(383, 546, 503, 586), file='./assets/reward/REWARD_3.png') REWARD_SAVE_CLICK = Button(area=(415, 184, 496, 214), color=(152, 150, 168), button=(415, 184, 496, 214), file='./assets/reward/REWARD_SAVE_CLICK.png') +TACTICAL_CLASS_START = Button(area=(1024, 590, 1197, 648), color=(96, 139, 194), button=(1024, 590, 1197, 648), file='./assets/reward/TACTICAL_CLASS_START.png') diff --git a/module/reward/commission.py b/module/reward/commission.py index d63fa77ba..fba635ec2 100644 --- a/module/reward/commission.py +++ b/module/reward/commission.py @@ -443,7 +443,7 @@ class RewardCommission(UI, InfoBarHandler, PopupHandler): logger.info('No commission chose') def handle_commission_start(self): - """Make sure current page is page_reward before calls. + """Remember to make sure current page is page_reward before calls. Returns: bool: If runs a commission. @@ -460,6 +460,7 @@ class RewardCommission(UI, InfoBarHandler, PopupHandler): self.commission_start() self.ui_goto(page_reward, skip_first_screenshot=True) + return True def commission_notice_show_at_campaign(self): """ diff --git a/module/reward/reward.py b/module/reward/reward.py index 818ec99e4..359336142 100644 --- a/module/reward/reward.py +++ b/module/reward/reward.py @@ -6,9 +6,10 @@ from module.logger import logger from module.reward.assets import * from module.ui.page import * from module.reward.commission import RewardCommission +from module.reward.tactical_class import RewardTacticalClass -class Reward(RewardCommission): +class Reward(RewardCommission, RewardTacticalClass): def reward(self): if not self.config.ENABLE_REWARD: return False @@ -21,6 +22,7 @@ class Reward(RewardCommission): self._reward_receive() self.handle_info_bar() self.handle_commission_start() + self.handle_tactical_class() self.ui_click( click_button=page_reward.links[page_main], @@ -47,7 +49,7 @@ class Reward(RewardCommission): Returns: bool: If rewarded. """ - logger.hr('Oil Reward') + logger.hr('Reward receive') reward = False exit_timer = Timer(1, count=3) @@ -75,15 +77,16 @@ class Reward(RewardCommission): reward = True continue - for button in btn: - if not click_timer.reached(): - continue - if self.appear_then_click(button, interval=1): - btn.remove(button) - exit_timer.reset() - click_timer.reset() - reward = True - continue + if click_timer.reached() and ( + (self.config.ENABLE_OIL_REWARD and self.appear_then_click(OIL, interval=60)) + or (self.config.ENABLE_COIN_REWARD and self.appear_then_click(COIN, interval=60)) + or (self.config.ENABLE_COMMISSION_REWARD and self.appear_then_click(REWARD_1, interval=1)) + or (self.config.ENABLE_REWARD and self.appear_then_click(REWARD_3, interval=1)) + ): + exit_timer.reset() + click_timer.reset() + reward = True + continue if not self.appear(page_reward.check_button) or self.info_bar_count(): exit_timer.reset() diff --git a/module/reward/tactical_class.py b/module/reward/tactical_class.py new file mode 100644 index 000000000..51a63b94b --- /dev/null +++ b/module/reward/tactical_class.py @@ -0,0 +1,247 @@ +import numpy as np +from scipy import signal + +from module.base.button import Button, ButtonGrid +from module.base.timer import Timer, time_range_active +from module.base.utils import area_offset, get_color, color_similar, color_similarity_2d +from module.handler.popup import PopupHandler +from module.logger import logger +from module.reward.assets import TACTICAL_CLASS_START, REWARD_2 +from module.ui.assets import TACTICAL_CHECK +from module.ui.ui import UI, page_tactical, page_reward + +GENRE_NAME_DICT = { + 1: 'Offensive', # red + 2: 'Defensive', # blue + 3: 'Support', # yellow +} +BOOKS_GRID = ButtonGrid(origin=(239, 288), delta=(140, 120), button_shape=(98, 98), grid_shape=(6, 2)) + + +class Book: + color_genre = { + 1: (214, 69, 74), # Offensive, red + 2: (115, 178, 255), # Defensive, blue + 3: (247, 190, 99), # Support, yellow + } + color_tier = { + 1: (104, 181, 238), # T1, blue + 2: (151, 129, 203), # T2, purple + 3: (235, 208, 120), # T3, gold + } + + def __init__(self, image, button): + """ + Args: + image (PIL.Image.Image): + button (Button): + """ + image = image.crop(button.area) + self.button = button + + self.genre = 0 + color = get_color(image, (65, 35, 72, 42)) + for key, value in self.color_genre.items(): + if color_similar(color1=color, color2=value, threshold=30): + self.genre = key + + self.tier = 0 + color = get_color(image, (83, 61, 92, 70)) + for key, value in self.color_tier.items(): + if color_similar(color1=color, color2=value, threshold=30): + self.tier = key + + color = color_similarity_2d(image.crop((15, 0, 97, 13)), color=(148, 251, 99)) + self.exp = bool(np.sum(color > 221) > 50) + + self.valid = bool(self.genre and self.tier) + + def __str__(self): + # Example: Defensive_T3_Exp + text = f'{GENRE_NAME_DICT.get(self.genre, "Unknown")}_T{self.tier}' + if self.exp: + text += '_Exp' + return text + + +class BookGroup: + def __init__(self, books): + """ + Args: + books (list[Book]): + """ + self.books = books + + def __iter__(self): + return iter(self.books) + + def __len__(self): + return len(self.books) + + def __bool__(self): + return len(self.books) > 0 + + def __getitem__(self, item): + return self.books[item] + + def __str__(self): + # return str([str(grid) for grid in self]) + return '[' + ', '.join([str(grid) for grid in self]) + ']' + + def select(self, **kwargs): + """ + Args: + **kwargs: Attributes of Grid. + + Returns: + SelectedGrids: + """ + result = [] + for grid in self.books: + flag = True + for k, v in kwargs.items(): + if grid.__getattribute__(k) != v: + flag = False + if flag: + result.append(grid) + + return BookGroup(result) + + def add(self, books): + """ + Args: + books(BookGroup): + + Returns: + BookGroup: + """ + return BookGroup(self.books + books.books) + + def choose(self, tier, exp=True): + """ + Args: + tier (int): Max tier to choose. + exp (bool): True to choose books with exp bonus, False to choose other books in the same tier. + + Returns: + Book: + """ + while 1: + books = self.select(tier=tier) + tier -= 1 + books_with_exp = books.select(exp=True) + if exp and not books_with_exp: + continue + if books_with_exp: + books = books_with_exp + if books: + logger.attr('Book_choose', books[0]) + return books[0] + + # End + if tier <= 0: + break + + logger.warning('No book choose, return first book.') + return self[0] + + +class RewardTacticalClass(UI, PopupHandler): + tactical_animation_timer = Timer(2, count=3) + + def _tactical_animation_running(self): + """ + Returns: + bool: If showing skill points increasing animation. + """ + color_height = np.mean(self.device.image.crop((922, 0, 1036, 720)).convert('L'), axis=1) + parameters = {'height': 200} + peaks, _ = signal.find_peaks(color_height, **parameters) + peaks = [y for y in peaks if y > 67 + 243] + + if not len(peaks): + logger.warning('No student card found.') + for y in peaks: + student_area = (447, y - 243, 1244, y) + area = area_offset((677, 172, 761, 183), student_area[0:2]) + # Normal: 160, In skill-increasing animation: 109 + if np.mean(get_color(self.device.image, area)) < 135: + return True + + return False + + def _tactical_books_choose(self): + """ + Choose tactical book according to config. + """ + books = BookGroup([Book(self.device.image, button) for button in BOOKS_GRID.buttons()]).select(valid=True) + logger.attr('Book_count', len(books)) + for index in range(1, 4): + logger.info(f'Book_T{index}: {books.select(tier=index)}') + if not books: + logger.warning('No book found.') + + if not time_range_active(self.config.TACTICAL_NIGHT_RANGE): + tier = self.config.TACTICAL_BOOK_TIER + exp = self.config.TACTICAL_EXP_FIRST + else: + tier = self.config.TACTICAL_BOOK_TIER_NIGHT + exp = self.config.TACTICAL_EXP_FIRST_NIGHT + book = books.choose(tier=tier, exp=exp) + + self.device.click(book.button) + self.device.sleep((0.3, 0.5)) + + def _tactical_class_receive(self, skip_first_screenshot=True): + """Remember to make sure current page is page_reward before calls. + + Args: + skip_first_screenshot (bool): + + Returns: + bool: If rewarded. + """ + if not self.appear(REWARD_2): + logger.info('No tactical class reward.') + return False + + logger.hr('Tactical class receive') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear_then_click(REWARD_2, interval=1): + continue + if self.handle_popup_confirm(): + continue + if self.appear(TACTICAL_CLASS_START, offset=(30, 30), interval=1): + self._tactical_books_choose() + self.device.click(TACTICAL_CLASS_START) + continue + + # End + if self.appear(TACTICAL_CHECK, offset=(20, 20)): + self.ui_current = page_tactical + if not self._tactical_animation_running(): + if self.tactical_animation_timer.reached(): + logger.info('Tactical reward end.') + break + else: + self.tactical_animation_timer.reset() + + self.ui_goto(page_reward, skip_first_screenshot=True) + return True + + def handle_tactical_class(self): + """ + Returns: + bool: If rewarded. + """ + if not self.config.ENABLE_TACTICAL_REWARD: + return False + + self._tactical_class_receive() + + return True diff --git a/module/ui/assets.py b/module/ui/assets.py index 4e0509e62..d6ecc5d21 100644 --- a/module/ui/assets.py +++ b/module/ui/assets.py @@ -24,4 +24,6 @@ MISSION_CHECK = Button(area=(120, 15, 173, 40), color=(141, 156, 194), button=(1 REWARD_CHECK = Button(area=(302, 119, 371, 195), color=(146, 118, 120), button=(302, 119, 371, 195), file='./assets/ui/REWARD_CHECK.png') REWARD_GOTO_COMMISSION = Button(area=(383, 262, 503, 302), color=(91, 136, 199), button=(383, 262, 503, 302), file='./assets/ui/REWARD_GOTO_COMMISSION.png') REWARD_GOTO_MAIN = Button(area=(1037, 611, 1107, 656), color=(134, 122, 127), button=(1037, 611, 1107, 656), file='./assets/ui/REWARD_GOTO_MAIN.png') +REWARD_GOTO_TACTICAL = Button(area=(383, 404, 503, 444), color=(89, 140, 198), button=(383, 404, 503, 444), file='./assets/ui/REWARD_GOTO_TACTICAL.png') SP_CHECK = Button(area=(123, 63, 206, 109), color=(95, 110, 145), button=(123, 63, 206, 109), file='./assets/ui/SP_CHECK.png') +TACTICAL_CHECK = Button(area=(122, 14, 231, 38), color=(145, 161, 200), button=(122, 14, 231, 38), file='./assets/ui/TACTICAL_CHECK.png') diff --git a/module/ui/page.py b/module/ui/page.py index 2a9d7e990..edd7bc963 100644 --- a/module/ui/page.py +++ b/module/ui/page.py @@ -78,6 +78,13 @@ page_commission.link(button=GOTO_MAIN, destination=page_main) page_commission.link(button=BACK_ARROW, destination=page_reward) page_reward.link(button=REWARD_GOTO_COMMISSION, destination=page_commission) +# Tactical class +# Please don't goto tactical class from academy. +page_tactical = Page(TACTICAL_CHECK) +page_tactical.link(button=GOTO_MAIN, destination=page_main) +page_tactical.link(button=BACK_ARROW, destination=page_reward) +page_reward.link(button=REWARD_GOTO_TACTICAL, destination=page_tactical) + # Event list page_event_list = Page(EVENT_LIST_CHECK) page_event_list.link(button=GOTO_MAIN, destination=page_main)