diff --git a/alas.py b/alas.py index bbc34638a..9df02d3b0 100644 --- a/alas.py +++ b/alas.py @@ -418,6 +418,10 @@ class AzurLaneAutoScript: GemsFarming(config=self.config, device=self.device).run( name=self.config.Campaign_Name, folder=self.config.Campaign_Event, mode=self.config.Campaign_Mode) + def daemon_15_1(self): + from module.daemon.daemon_15_1 import AzurLaneDaemon + AzurLaneDaemon(config=self.config, device=self.device, task="Daemon_15_1").run() + def daemon(self): from module.daemon.daemon import AzurLaneDaemon AzurLaneDaemon(config=self.config, device=self.device, task="Daemon").run() diff --git a/assets/cn/combat/BATTLE_TIME.png b/assets/cn/combat/BATTLE_TIME.png new file mode 100644 index 000000000..e3fd5d6ef Binary files /dev/null and b/assets/cn/combat/BATTLE_TIME.png differ diff --git a/assets/cn/combat/MOVE_LEFT.png b/assets/cn/combat/MOVE_LEFT.png new file mode 100644 index 000000000..3f37d0a81 Binary files /dev/null and b/assets/cn/combat/MOVE_LEFT.png differ diff --git a/config/template.json b/config/template.json index f710085ba..bd7eeae70 100644 --- a/config/template.json +++ b/config/template.json @@ -2086,6 +2086,18 @@ "Storage": {} } }, + "Daemon_15_1": { + "Daemon_15_1": { + "AutoCombat": false, + "QuitTime": "02:00", + "MoveDownTime": 1, + "RunCount": 0, + "QuitScreenshot": false + }, + "Storage": { + "Storage": {} + } + }, "Daemon": { "Daemon": { "EnterMap": true diff --git a/dev_tools/plane_statistics.py b/dev_tools/plane_statistics.py new file mode 100644 index 000000000..9172c02fc --- /dev/null +++ b/dev_tools/plane_statistics.py @@ -0,0 +1,143 @@ +import csv +from tqdm import tqdm + +from module.base.button import ButtonGrid +from module.base.decorator import cached_property, run_once +from module.base.utils import load_image +from module.combat.assets import BATTLE_TIME +from module.daemon.daemon_15_1 import BattleTime as BattleTime_ +from module.logger import logger +from module.ocr.al_ocr import AlOcr +from module.ocr.ocr import Ocr, Digit +from module.statistics.utils import * + + +class BattleTime(BattleTime_): + @staticmethod + def parse_time(string): + return string + + +class PlaneOcr(Digit): + def __init__(self, buttons, lang='azur_lane', letter=(255, 251, 247), threshold=128, alphabet='0123456789IDSBX', + name=None): + super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) + + def after_process(self, result): + result = result.replace('X', '') if 'X' in result else '0' + result = super().after_process(result) + return result + + +class PlaneStatistics: + DROP_FOLDER = './screenshots' + CNOCR_CONTEXT = 'cpu' + CSV_FILE = 'drop_result.csv' + CSV_OVERWRITE = True + CSV_ENCODING = 'utf-8' + PLANE_ROWS = 7 + PLANE_GRID = ButtonGrid(origin=(1230, 90), button_shape=(48, 30), grid_shape=(1, 7), delta=(0, 43)) + + def __init__(self): + AlOcr.CNOCR_CONTEXT = PlaneStatistics.CNOCR_CONTEXT + Ocr.SHOW_LOG = False + self.PLANE_GRID.grid_shape = (1, self.PLANE_ROWS) + self.place_ocr_model = PlaneOcr(self.PLANE_GRID.buttons) + self.time_ocr_model = BattleTime(BATTLE_TIME) + + @property + def csv_file(self): + return os.path.join(PlaneStatistics.DROP_FOLDER, PlaneStatistics.CSV_FILE) + + @staticmethod + def drop_folder(campaign): + return os.path.join(PlaneStatistics.DROP_FOLDER, campaign) + + @cached_property + def csv_overwrite_check(self): + """ + Remove existing csv file. This method only run once. + """ + if PlaneStatistics.CSV_OVERWRITE: + if os.path.exists(self.csv_file): + logger.info(f'Remove existing csv file: {self.csv_file}') + os.remove(self.csv_file) + return True + + @staticmethod + @run_once + def csv_write_column_name(writer, columns): + writer.writerows([columns]) + + def parse_plane(self, file): + ts = os.path.splitext(os.path.basename(file))[0] + campaign = os.path.basename(os.path.abspath(os.path.join(file, '../'))) + campaign = campaign.replace('campaign_', '') + images = unpack(load_image(file)) + image = images[0] + plane = self.place_ocr_model.ocr(image) + time = self.time_ocr_model.ocr(image) + yield ['\'' + ts, campaign, str(plane), sum(plane), time] + + def extract_plane(self, campaign): + """ + Extract images from a given folder. + + Args: + campaign (str): + """ + print('') + logger.hr(f'extract plane statistics from {campaign}', level=1) + _ = self.csv_overwrite_check + + with open(self.csv_file, 'a', newline='', encoding=PlaneStatistics.CSV_ENCODING) as csv_file: + writer = csv.writer(csv_file) + self.csv_write_column_name(writer, ['截图文件名称', '关卡名称', 'ocr识别结果', '飞机总数', '右上角时间']) + for ts, file in tqdm(load_folder(self.drop_folder(campaign)).items()): + try: + rows = self.parse_plane(file) + writer.writerows(rows) + except ImageError as e: + logger.warning(e) + continue + except Exception as e: + logger.exception(e) + logger.warning(f'Error on image {ts}') + continue + + +if __name__ == '__main__': + # Drop screenshot folder. Default to './screenshots' + # 截图文件夹名称 + PlaneStatistics.DROP_FOLDER = './screenshots' + # 'cpu' or 'gpu', default to 'cpu'. + # Use 'gpu' for faster prediction, but you must have the gpu version of mxnet installed. + PlaneStatistics.CNOCR_CONTEXT = 'cpu' + # Name of the output csv file. + # This will write to {DROP_FOLDER}/{CSV_FILE}. + # 结果文件名称 + PlaneStatistics.CSV_FILE = 'plane_result.csv' + # If True, remove existing file before extraction. + # 是否覆写csv + PlaneStatistics.CSV_OVERWRITE = True + # Usually to be 'utf-8'. + # For better Chinese export to Excel, use 'gbk'. + PlaneStatistics.CSV_ENCODING = 'gbk' + # Default to be 7. + # This will change the ocr of rows of plane. + # 飞机识别行数(最多有几行飞机) + PlaneStatistics.PLANE_ROWS = 9 + # campaign names to export under DROP_FOLDER. + # This will load {DROP_FOLDER}/{CAMPAIGN}. + # Just a demonstration here, you should modify it to your own. + # 关卡名称(截图文件夹内的需要识别图片的文件夹的名称) + CAMPAIGNS = ['campaign_15_1'] + + stat = PlaneStatistics() + + """ + Step 1: + Run this code. + """ + for i in CAMPAIGNS: + stat.extract_plane(i) diff --git a/module/combat/assets.py b/module/combat/assets.py index 76ff87e60..cc45bff91 100644 --- a/module/combat/assets.py +++ b/module/combat/assets.py @@ -17,6 +17,7 @@ BATTLE_STATUS_B = Button(area={'cn': (625, 297, 712, 317), 'en': (625, 297, 712, BATTLE_STATUS_C = Button(area={'cn': (625, 211, 647, 297), 'en': (625, 211, 647, 297), 'jp': (625, 211, 647, 297), 'tw': (625, 211, 647, 297)}, color={'cn': (199, 208, 198), 'en': (199, 208, 198), 'jp': (199, 208, 198), 'tw': (199, 208, 198)}, button={'cn': (1000, 631, 1055, 689), 'en': (1000, 631, 1055, 689), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/BATTLE_STATUS_C.png', 'en': './assets/en/combat/BATTLE_STATUS_C.png', 'jp': './assets/jp/combat/BATTLE_STATUS_C.png', 'tw': './assets/tw/combat/BATTLE_STATUS_C.png'}) BATTLE_STATUS_D = Button(area={'cn': (618, 191, 639, 317), 'en': (618, 191, 639, 317), 'jp': (618, 191, 639, 317), 'tw': (618, 191, 639, 317)}, color={'cn': (199, 208, 199), 'en': (199, 208, 199), 'jp': (199, 208, 199), 'tw': (199, 208, 199)}, button={'cn': (1000, 631, 1055, 689), 'en': (1000, 631, 1055, 689), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/BATTLE_STATUS_D.png', 'en': './assets/en/combat/BATTLE_STATUS_D.png', 'jp': './assets/jp/combat/BATTLE_STATUS_D.png', 'tw': './assets/tw/combat/BATTLE_STATUS_D.png'}) BATTLE_STATUS_S = Button(area={'cn': (643, 297, 722, 317), 'en': (643, 297, 722, 317), 'jp': (643, 297, 722, 317), 'tw': (643, 297, 722, 317)}, color={'cn': (233, 242, 127), 'en': (233, 242, 127), 'jp': (233, 242, 127), 'tw': (233, 242, 127)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/BATTLE_STATUS_S.png', 'en': './assets/en/combat/BATTLE_STATUS_S.png', 'jp': './assets/jp/combat/BATTLE_STATUS_S.png', 'tw': './assets/tw/combat/BATTLE_STATUS_S.png'}) +BATTLE_TIME = Button(area={'cn': (1062, 36, 1119, 59), 'en': (1062, 36, 1119, 59), 'jp': (1062, 36, 1119, 59), 'tw': (1062, 36, 1119, 59)}, color={'cn': (98, 158, 99), 'en': (98, 158, 99), 'jp': (98, 158, 99), 'tw': (98, 158, 99)}, button={'cn': (1062, 36, 1119, 59), 'en': (1062, 36, 1119, 59), 'jp': (1062, 36, 1119, 59), 'tw': (1062, 36, 1119, 59)}, file={'cn': './assets/cn/combat/BATTLE_TIME.png', 'en': './assets/cn/combat/BATTLE_TIME.png', 'jp': './assets/cn/combat/BATTLE_TIME.png', 'tw': './assets/cn/combat/BATTLE_TIME.png'}) COMBAT_AUTO = Button(area={'cn': (136, 573, 167, 604), 'en': (136, 573, 167, 604), 'jp': (136, 573, 167, 604), 'tw': (136, 573, 167, 604)}, color={'cn': (229, 242, 255), 'en': (229, 242, 255), 'jp': (229, 242, 255), 'tw': (229, 242, 255)}, button={'cn': (136, 573, 167, 604), 'en': (136, 573, 167, 604), 'jp': (136, 573, 167, 604), 'tw': (136, 573, 167, 604)}, file={'cn': './assets/cn/combat/COMBAT_AUTO.png', 'en': './assets/en/combat/COMBAT_AUTO.png', 'jp': './assets/jp/combat/COMBAT_AUTO.png', 'tw': './assets/tw/combat/COMBAT_AUTO.png'}) COMBAT_AUTO_133 = Button(area={'cn': (131, 568, 170, 609), 'en': (131, 568, 170, 609), 'jp': (131, 568, 170, 609), 'tw': (131, 568, 170, 609)}, color={'cn': (234, 244, 255), 'en': (234, 244, 255), 'jp': (234, 244, 255), 'tw': (234, 244, 255)}, button={'cn': (131, 568, 170, 609), 'en': (131, 568, 170, 609), 'jp': (131, 568, 170, 609), 'tw': (131, 568, 170, 609)}, file={'cn': './assets/cn/combat/COMBAT_AUTO_133.png', 'en': './assets/en/combat/COMBAT_AUTO_133.png', 'jp': './assets/jp/combat/COMBAT_AUTO_133.png', 'tw': './assets/tw/combat/COMBAT_AUTO_133.png'}) COMBAT_AUTO_150 = Button(area={'cn': (129, 567, 172, 611), 'en': (129, 567, 172, 611), 'jp': (129, 567, 172, 611), 'tw': (129, 567, 172, 611)}, color={'cn': (238, 247, 255), 'en': (238, 247, 255), 'jp': (238, 247, 255), 'tw': (238, 247, 255)}, button={'cn': (129, 567, 172, 611), 'en': (129, 567, 172, 611), 'jp': (129, 567, 172, 611), 'tw': (129, 567, 172, 611)}, file={'cn': './assets/cn/combat/COMBAT_AUTO_150.png', 'en': './assets/en/combat/COMBAT_AUTO_150.png', 'jp': './assets/jp/combat/COMBAT_AUTO_150.png', 'tw': './assets/tw/combat/COMBAT_AUTO_150.png'}) @@ -38,6 +39,7 @@ GET_SHIP = Button(area={'cn': (1104, 610, 1110, 630), 'en': (1104, 610, 1110, 63 LOADING_BAR = Button(area={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, color={'cn': (172, 205, 232), 'en': (172, 205, 232), 'jp': (172, 205, 232), 'tw': (172, 205, 232)}, button={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, file={'cn': './assets/cn/combat/LOADING_BAR.png', 'en': './assets/en/combat/LOADING_BAR.png', 'jp': './assets/jp/combat/LOADING_BAR.png', 'tw': './assets/tw/combat/LOADING_BAR.png'}) MAIN_FLEET_POWER_ZERO = Button(area={'cn': (131, 151, 232, 206), 'en': (131, 151, 232, 206), 'jp': (131, 151, 232, 206), 'tw': (131, 151, 232, 206)}, color={'cn': (63, 79, 98), 'en': (63, 79, 98), 'jp': (63, 79, 98), 'tw': (63, 79, 98)}, button={'cn': (131, 151, 232, 206), 'en': (131, 151, 232, 206), 'jp': (131, 151, 232, 206), 'tw': (131, 151, 232, 206)}, file={'cn': './assets/cn/combat/MAIN_FLEET_POWER_ZERO.png', 'en': './assets/en/combat/MAIN_FLEET_POWER_ZERO.png', 'jp': './assets/jp/combat/MAIN_FLEET_POWER_ZERO.png', 'tw': './assets/tw/combat/MAIN_FLEET_POWER_ZERO.png'}) MOVE_DOWN = Button(area={'cn': (148, 647, 155, 669), 'en': (148, 647, 155, 669), 'jp': (148, 647, 155, 669), 'tw': (148, 647, 155, 669)}, color={'cn': (21, 28, 57), 'en': (21, 28, 57), 'jp': (21, 28, 57), 'tw': (21, 28, 57)}, button={'cn': (148, 647, 155, 669), 'en': (148, 647, 155, 669), 'jp': (148, 647, 155, 669), 'tw': (148, 647, 155, 669)}, file={'cn': './assets/cn/combat/MOVE_DOWN.png', 'en': './assets/en/combat/MOVE_DOWN.png', 'jp': './assets/jp/combat/MOVE_DOWN.png', 'tw': './assets/tw/combat/MOVE_DOWN.png'}) +MOVE_LEFT = Button(area={'cn': (3, 585, 27, 591), 'en': (3, 585, 27, 591), 'jp': (3, 585, 27, 591), 'tw': (3, 585, 27, 591)}, color={'cn': (35, 108, 156), 'en': (35, 108, 156), 'jp': (35, 108, 156), 'tw': (35, 108, 156)}, button={'cn': (3, 585, 27, 591), 'en': (3, 585, 27, 591), 'jp': (3, 585, 27, 591), 'tw': (3, 585, 27, 591)}, file={'cn': './assets/cn/combat/MOVE_LEFT.png', 'en': './assets/cn/combat/MOVE_LEFT.png', 'jp': './assets/cn/combat/MOVE_LEFT.png', 'tw': './assets/cn/combat/MOVE_LEFT.png'}) MOVE_LEFT_DOWN = Button(area={'cn': (67, 668, 112, 707), 'en': (67, 668, 112, 707), 'jp': (67, 668, 112, 707), 'tw': (67, 668, 112, 707)}, color={'cn': (65, 80, 100), 'en': (65, 80, 100), 'jp': (65, 80, 100), 'tw': (65, 80, 100)}, button={'cn': (67, 668, 112, 707), 'en': (67, 668, 112, 707), 'jp': (67, 668, 112, 707), 'tw': (67, 668, 112, 707)}, file={'cn': './assets/cn/combat/MOVE_LEFT_DOWN.png', 'en': './assets/en/combat/MOVE_LEFT_DOWN.png', 'jp': './assets/jp/combat/MOVE_LEFT_DOWN.png', 'tw': './assets/tw/combat/MOVE_LEFT_DOWN.png'}) NEW_SHIP = Button(area={'cn': (206, 87, 213, 93), 'en': (206, 87, 213, 93), 'jp': (206, 87, 213, 93), 'tw': (206, 87, 213, 93)}, color={'cn': (235, 171, 60), 'en': (235, 171, 60), 'jp': (235, 171, 60), 'tw': (235, 171, 60)}, button={'cn': (206, 87, 213, 93), 'en': (206, 87, 213, 93), 'jp': (206, 87, 213, 93), 'tw': (206, 87, 213, 93)}, file={'cn': './assets/cn/combat/NEW_SHIP.png', 'en': './assets/en/combat/NEW_SHIP.png', 'jp': './assets/jp/combat/NEW_SHIP.png', 'tw': './assets/tw/combat/NEW_SHIP.png'}) OPTS_INFO_D = Button(area={'cn': (601, 151, 704, 178), 'en': (565, 143, 692, 179), 'jp': (512, 154, 605, 176), 'tw': (602, 152, 702, 177)}, color={'cn': (158, 110, 113), 'en': (171, 116, 110), 'jp': (201, 187, 191), 'tw': (164, 130, 137)}, button={'cn': (583, 605, 677, 628), 'en': (590, 587, 627, 647), 'jp': (574, 596, 685, 635), 'tw': (583, 604, 676, 627)}, file={'cn': './assets/cn/combat/OPTS_INFO_D.png', 'en': './assets/en/combat/OPTS_INFO_D.png', 'jp': './assets/jp/combat/OPTS_INFO_D.png', 'tw': './assets/tw/combat/OPTS_INFO_D.png'}) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 1dbbac12c..9df4ed49f 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -9982,6 +9982,38 @@ } } }, + "Daemon_15_1": { + "Daemon_15_1": { + "AutoCombat": { + "type": "checkbox", + "value": false + }, + "QuitTime": { + "type": "input", + "value": "02:00" + }, + "MoveDownTime": { + "type": "input", + "value": 1 + }, + "RunCount": { + "type": "input", + "value": 0 + }, + "QuitScreenshot": { + "type": "checkbox", + "value": false + } + }, + "Storage": { + "Storage": { + "type": "storage", + "value": {}, + "valuetype": "ignore", + "display": "disabled" + } + } + }, "Daemon": { "Daemon": { "EnterMap": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 0f2a200d9..12cee038e 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -779,6 +779,12 @@ OpsiHazard1Leveling: # ==================== Tools ==================== +Daemon_15_1: + AutoCombat: false + QuitTime: 02:00 + MoveDownTime: 1 + RunCount: 0 + QuitScreenshot: false Daemon: EnterMap: true OpsiDaemon: diff --git a/module/config/argument/menu.json b/module/config/argument/menu.json index 334da73b2..25e9ef437 100644 --- a/module/config/argument/menu.json +++ b/module/config/argument/menu.json @@ -101,6 +101,7 @@ "menu": "collapse", "page": "tool", "tasks": [ + "Daemon_15_1", "Daemon", "OpsiDaemon", "EventStory", diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index a0a46c810..ccd7a88a1 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -338,6 +338,8 @@ Tool: menu: 'collapse' page: 'tool' tasks: + Daemon_15_1: + - Daemon_15_1 Daemon: - Daemon OpsiDaemon: diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 52849df23..81a9ae391 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -460,6 +460,13 @@ class GeneratedConfig: OpsiHazard1Leveling_OperationCoinsPreserve = 100000 OpsiHazard1Leveling_DoScanningDevice = False + # Group `Daemon_15_1` + Daemon_15_1_AutoCombat = False + Daemon_15_1_QuitTime = '02:00' + Daemon_15_1_MoveDownTime = 1 + Daemon_15_1_RunCount = 0 + Daemon_15_1_QuitScreenshot = False + # Group `Daemon` Daemon_EnterMap = True diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 6f6480499..27ef75846 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -258,6 +258,10 @@ "name": "Cross Month Daily", "help": " ALAS will enter OpSi 10min before OpSi reset, wait until OpSi reset but not exit OpSi. Then do the daily, obscure, abyssal and meowfficer farming to get extra gold plates. When running dailies, settings in task \"OpSiDaily\" are used, the rest function are the same.\n IMPORTANT: Please do not touch the game while ALAS is waiting for OpSi reset." }, + "Daemon_15_1": { + "name": "Task.Daemon_15_1.name", + "help": "Task.Daemon_15_1.help" + }, "Daemon": { "name": "Normal Semi-auto", "help": "" @@ -2656,6 +2660,32 @@ "help": "Exchange purple coins to operation coins, which may cause a shortage of purple coins" } }, + "Daemon_15_1": { + "_info": { + "name": "Daemon_15_1._info.name", + "help": "Daemon_15_1._info.help" + }, + "AutoCombat": { + "name": "Daemon_15_1.AutoCombat.name", + "help": "Daemon_15_1.AutoCombat.help" + }, + "QuitTime": { + "name": "Daemon_15_1.QuitTime.name", + "help": "Daemon_15_1.QuitTime.help" + }, + "MoveDownTime": { + "name": "Daemon_15_1.MoveDownTime.name", + "help": "Daemon_15_1.MoveDownTime.help" + }, + "RunCount": { + "name": "Daemon_15_1.RunCount.name", + "help": "Daemon_15_1.RunCount.help" + }, + "QuitScreenshot": { + "name": "Daemon_15_1.QuitScreenshot.name", + "help": "Daemon_15_1.QuitScreenshot.help" + } + }, "Daemon": { "_info": { "name": "Semi-auto Clicking", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 2d13c83e7..569bc53f4 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -258,6 +258,10 @@ "name": "Cross Month Daily", "help": " ALAS will enter OpSi 10min before OpSi reset, wait until OpSi reset but not exit OpSi. Then do the daily, obscure, abyssal and meowfficer farming to get extra gold plates. When running dailies, settings in task \"OpSiDaily\" are used, the rest function are the same.\n IMPORTANT: Please do not touch the game while ALAS is waiting for OpSi reset." }, + "Daemon_15_1": { + "name": "Task.Daemon_15_1.name", + "help": "Task.Daemon_15_1.help" + }, "Daemon": { "name": "半自動クリック", "help": "" @@ -2656,6 +2660,32 @@ "help": "OpsiHazard1Leveling.DoScanningDevice.help" } }, + "Daemon_15_1": { + "_info": { + "name": "Daemon_15_1._info.name", + "help": "Daemon_15_1._info.help" + }, + "AutoCombat": { + "name": "Daemon_15_1.AutoCombat.name", + "help": "Daemon_15_1.AutoCombat.help" + }, + "QuitTime": { + "name": "Daemon_15_1.QuitTime.name", + "help": "Daemon_15_1.QuitTime.help" + }, + "MoveDownTime": { + "name": "Daemon_15_1.MoveDownTime.name", + "help": "Daemon_15_1.MoveDownTime.help" + }, + "RunCount": { + "name": "Daemon_15_1.RunCount.name", + "help": "Daemon_15_1.RunCount.help" + }, + "QuitScreenshot": { + "name": "Daemon_15_1.QuitScreenshot.name", + "help": "Daemon_15_1.QuitScreenshot.help" + } + }, "Daemon": { "_info": { "name": "Daemon._info.name", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 4196a744c..5dc0cd081 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -258,6 +258,10 @@ "name": "跨月每日", "help": " Alas将在大世界跨月重置之前10分钟进入大世界,等待大世界重置但不退出大世界,然后完成新一天的大世界每日、隐秘海域、深渊海域和短猫相接,以获得额外的金菜。运行大世界每日时,按\"大世界每日\"任务设置运行,其余同理。\n 重要:Alas等待跨月期间,请不要操作游戏。" }, + "Daemon_15_1": { + "name": "15-1测试", + "help": "" + }, "Daemon": { "name": "半自动点击", "help": "" @@ -2656,6 +2660,32 @@ "help": "消耗紫币换取黄币,可能会导致紫币不足" } }, + "Daemon_15_1": { + "_info": { + "name": "15-1支援舰队测试用", + "help": "配好队,关闭自律,进图后关闭阵容锁定,手动选择一个需要测试的敌人,然后运行该任务\n注意:只能使用经典战斗ui(老战斗ui)!!!\n使用结束后需要手动关闭此任务" + }, + "AutoCombat": { + "name": "使用自律战斗", + "help": "" + }, + "QuitTime": { + "name": "截图并退出关卡的时间", + "help": "如2:00, 02:30等" + }, + "MoveDownTime": { + "name": "非自律开局向下拉的时间(秒)", + "help": "在非自律战斗下,开局按向下按键的时长(秒)\n根据前排航速的不同,手动修改这个值确保前排在旗舰头像下方,例如:\n1s:埃吉尔 岛风 安克雷奇\n1.5s:埃吉尔 圣女贞德 安克雷奇" + }, + "RunCount": { + "name": "测试次数大于 X 后停止", + "help": "每运行一次,次数减一,归零后停止任务\n0 表示不限制次数" + }, + "QuitScreenshot": { + "name": "保存退出界面截图", + "help": "开启后,会额外保存一份退出界面时的截图,以供测试使用\n大部分情况不需要开启这个选项" + } + }, "Daemon": { "_info": { "name": "半自动点击", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index a05e36cce..be95ac223 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -258,6 +258,10 @@ "name": "跨月每日", "help": " Alas將在大世界跨月重置之前10分鐘進入大世界,等待大世界重置但不退出大世界,然後完成新一天的大世界每日、隱秘海域、深淵海域和短貓相接,以獲得額外的金菜。運行大世界每日時,按\"大世界每日\"任務設定運行,其餘同理。\n 重要:Alas等待跨月期間,請不要操作遊戲。" }, + "Daemon_15_1": { + "name": "Task.Daemon_15_1.name", + "help": "Task.Daemon_15_1.help" + }, "Daemon": { "name": "半自動點擊", "help": "" @@ -2656,6 +2660,32 @@ "help": "消耗紫幣換取黃幣,可能會導致紫幣不足" } }, + "Daemon_15_1": { + "_info": { + "name": "Daemon_15_1._info.name", + "help": "Daemon_15_1._info.help" + }, + "AutoCombat": { + "name": "Daemon_15_1.AutoCombat.name", + "help": "Daemon_15_1.AutoCombat.help" + }, + "QuitTime": { + "name": "Daemon_15_1.QuitTime.name", + "help": "Daemon_15_1.QuitTime.help" + }, + "MoveDownTime": { + "name": "Daemon_15_1.MoveDownTime.name", + "help": "Daemon_15_1.MoveDownTime.help" + }, + "RunCount": { + "name": "Daemon_15_1.RunCount.name", + "help": "Daemon_15_1.RunCount.help" + }, + "QuitScreenshot": { + "name": "Daemon_15_1.QuitScreenshot.name", + "help": "Daemon_15_1.QuitScreenshot.help" + } + }, "Daemon": { "_info": { "name": "半自動點擊", diff --git a/module/daemon/daemon_15_1.py b/module/daemon/daemon_15_1.py new file mode 100644 index 000000000..6531ec063 --- /dev/null +++ b/module/daemon/daemon_15_1.py @@ -0,0 +1,157 @@ +from datetime import datetime, timedelta +import re + + +from module.base.timer import Timer +from module.base.utils import copy_image +from module.campaign.campaign_base import CampaignBase +from module.combat.assets import BATTLE_TIME, MOVE_DOWN, MOVE_LEFT +from module.combat_ui.assets import QUIT +from module.daemon.daemon_base import DaemonBase +from module.exception import CampaignEnd +from module.exercise.assets import QUIT_RECONFIRM +from module.handler.ambush import MAP_AMBUSH_EVADE +from module.logger import logger +from module.map.assets import MAP_OFFENSIVE +from module.ocr.ocr import Duration + + +class BattleTime(Duration): + SHOW_LOG = False + + def __init__(self, buttons, lang='azur_lane', letter=(148, 255, 99), threshold=128, alphabet='0123456789:IDSB', + name=None): + super().__init__(buttons, lang=lang, letter=letter, threshold=threshold, alphabet=alphabet, name=name) + + @staticmethod + def parse_time(string): + result = re.search(r'(\d{1,2}):?(\d{2})', string) + if result: + result = [int(s) for s in result.groups()] + return timedelta(hours=0, minutes=result[0], seconds=result[1]) + else: + logger.warning(f'Invalid duration: {string}') + return timedelta(hours=0, minutes=0, seconds=0) + + +class AzurLaneDaemon(DaemonBase, CampaignBase): + battle_time_ocr_model = BattleTime(BATTLE_TIME) + + @property + def battle_time(self): + return self.battle_time_ocr_model.ocr(self.device.image).total_seconds() + + @property + def quit_time(self): + string = self.config.Daemon_15_1_QuitTime + string = string.strip().replace(':', ':') + t = datetime.strptime(string, "%M:%S") + return timedelta(hours=t.hour, minutes=t.minute, seconds=t.second).total_seconds() + + def run(self): + move = True + is_limit = False + end = False + self.device.screenshot_interval_set() + self.config.override(Emulator_ControlMethod='uiautomator2') + while 1: + self.device.screenshot() + + # End + if is_limit and self.config.Daemon_15_1_RunCount <= 0: + logger.hr('Triggered stop condition: Run count') + self.config.Daemon_15_1_RunCount = 0 + end = True + is_limit = self.config.Daemon_15_1_RunCount + pause = self.is_combat_executing() + # running a combat + if pause: + if not self.config.Daemon_15_1_AutoCombat and move: + move = False + self.device.long_click(MOVE_DOWN, duration=self.config.Daemon_15_1_MoveDownTime) + self.device.long_click(MOVE_LEFT, duration=(3, 4)) + continue + + # End + battle_time = self.battle_time + if battle_time and battle_time <= self.quit_time: + with self.stat.new(genre='campaign_15_1', method='save') as record: + if self.config.Daemon_15_1_RunCount: + self.config.Daemon_15_1_RunCount -= 1 + combat_image = copy_image(self.device.image) + + self.device.screenshot_interval_set() + skip_first_screenshot = True + pause_interval = Timer(0.5, count=1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if pause_interval.reached(): + pause = self.is_combat_executing() + if pause: + self.device.click(pause) + pause_interval.reset() + continue + + if QUIT.match_luma(self.device.image, offset=(20, 20)): + record.add(combat_image) + if self.config.Daemon_15_1_QuitScreenshot: + record.add(self.device.image) + break + continue + + # Quit + if self.handle_combat_quit(): + continue + if self.appear_then_click(QUIT_RECONFIRM, offset=(20, 20), interval=5): + move = True + if end: + break + continue + + # Combat + if self.combat_appear(): + self.combat_preparation(auto='combat_auto' if self.config.Daemon_15_1_AutoCombat else '') + try: + if self.handle_battle_status(): + self.combat_status(expected_end='no_searching') + continue + except CampaignEnd: + continue + + # Map operation + if self.appear_then_click(MAP_AMBUSH_EVADE, offset=(20, 20)): + self.device.sleep(1) + continue + if self.handle_mystery_items(): + continue + + # Retire + if self.handle_retirement(): + continue + + # Emotion + pass + + # Urgent commission + if self.handle_urgent_commission(): + continue + + # Popups + if self.handle_guild_popup_cancel(): + return True + if self.handle_vote_popup(): + continue + + # Story + if self.story_skip(): + continue + + # Map Offensive + if not end and self.appear_then_click(MAP_OFFENSIVE, interval=2): + continue + + return True diff --git a/module/submodule/utils.py b/module/submodule/utils.py index cbe65139c..f0ed68ddf 100644 --- a/module/submodule/utils.py +++ b/module/submodule/utils.py @@ -15,6 +15,7 @@ MOD_CONFIG_DICT = {} def get_available_func(): return ( + 'Daemon_15_1', 'Daemon', 'OpsiDaemon', 'EventStory',