diff --git a/assets/character/Arlan.png b/assets/character/Arlan.png new file mode 100644 index 000000000..5b6705a40 Binary files /dev/null and b/assets/character/Arlan.png differ diff --git a/assets/character/Asta.png b/assets/character/Asta.png new file mode 100644 index 000000000..42231ede3 Binary files /dev/null and b/assets/character/Asta.png differ diff --git a/assets/character/Bailu.png b/assets/character/Bailu.png new file mode 100644 index 000000000..1190175c4 Binary files /dev/null and b/assets/character/Bailu.png differ diff --git a/assets/character/Bronya.png b/assets/character/Bronya.png new file mode 100644 index 000000000..c699f29da Binary files /dev/null and b/assets/character/Bronya.png differ diff --git a/assets/character/CaelumtheDestruction.png b/assets/character/CaelumtheDestruction.png new file mode 100644 index 000000000..0feba9083 Binary files /dev/null and b/assets/character/CaelumtheDestruction.png differ diff --git a/assets/character/CaelumthePreservation.png b/assets/character/CaelumthePreservation.png new file mode 100644 index 000000000..182f4eb1d Binary files /dev/null and b/assets/character/CaelumthePreservation.png differ diff --git a/assets/character/Clara.png b/assets/character/Clara.png new file mode 100644 index 000000000..9938df931 Binary files /dev/null and b/assets/character/Clara.png differ diff --git a/assets/character/DanHeng.png b/assets/character/DanHeng.png new file mode 100644 index 000000000..c8b3d6188 Binary files /dev/null and b/assets/character/DanHeng.png differ diff --git a/assets/character/Gepard.png b/assets/character/Gepard.png new file mode 100644 index 000000000..22081031d Binary files /dev/null and b/assets/character/Gepard.png differ diff --git a/assets/character/Herta.png b/assets/character/Herta.png new file mode 100644 index 000000000..6f8a1f8b3 Binary files /dev/null and b/assets/character/Herta.png differ diff --git a/assets/character/Himeko.png b/assets/character/Himeko.png new file mode 100644 index 000000000..67c565b9a Binary files /dev/null and b/assets/character/Himeko.png differ diff --git a/assets/character/Hook.png b/assets/character/Hook.png new file mode 100644 index 000000000..0c5272a14 Binary files /dev/null and b/assets/character/Hook.png differ diff --git a/assets/character/JingYuan.png b/assets/character/JingYuan.png new file mode 100644 index 000000000..aa101a7f7 Binary files /dev/null and b/assets/character/JingYuan.png differ diff --git a/assets/character/Luocha.png b/assets/character/Luocha.png new file mode 100644 index 000000000..12dc357ab Binary files /dev/null and b/assets/character/Luocha.png differ diff --git a/assets/character/March7th.png b/assets/character/March7th.png new file mode 100644 index 000000000..1c7cc2169 Binary files /dev/null and b/assets/character/March7th.png differ diff --git a/assets/character/Natasha.png b/assets/character/Natasha.png new file mode 100644 index 000000000..71285ab53 Binary files /dev/null and b/assets/character/Natasha.png differ diff --git a/assets/character/Pela.png b/assets/character/Pela.png new file mode 100644 index 000000000..d6d8e41e1 Binary files /dev/null and b/assets/character/Pela.png differ diff --git a/assets/character/Qingque.png b/assets/character/Qingque.png new file mode 100644 index 000000000..c701b0e2e Binary files /dev/null and b/assets/character/Qingque.png differ diff --git a/assets/character/Sampo.png b/assets/character/Sampo.png new file mode 100644 index 000000000..5ef62431e Binary files /dev/null and b/assets/character/Sampo.png differ diff --git a/assets/character/Seele.png b/assets/character/Seele.png new file mode 100644 index 000000000..25bc6f9c5 Binary files /dev/null and b/assets/character/Seele.png differ diff --git a/assets/character/Serval.png b/assets/character/Serval.png new file mode 100644 index 000000000..e646e8f90 Binary files /dev/null and b/assets/character/Serval.png differ diff --git a/assets/character/SilverWolf.png b/assets/character/SilverWolf.png new file mode 100644 index 000000000..9ab2c95c8 Binary files /dev/null and b/assets/character/SilverWolf.png differ diff --git a/assets/character/StelletheDestruction.png b/assets/character/StelletheDestruction.png new file mode 100644 index 000000000..cdacb9d35 Binary files /dev/null and b/assets/character/StelletheDestruction.png differ diff --git a/assets/character/StellethePreservation.png b/assets/character/StellethePreservation.png new file mode 100644 index 000000000..f933f6bcb Binary files /dev/null and b/assets/character/StellethePreservation.png differ diff --git a/assets/character/Sushang.png b/assets/character/Sushang.png new file mode 100644 index 000000000..5a4cd0a73 Binary files /dev/null and b/assets/character/Sushang.png differ diff --git a/assets/character/Tingyun.png b/assets/character/Tingyun.png new file mode 100644 index 000000000..0eeeb7b01 Binary files /dev/null and b/assets/character/Tingyun.png differ diff --git a/assets/character/Welt.png b/assets/character/Welt.png new file mode 100644 index 000000000..278aa7d47 Binary files /dev/null and b/assets/character/Welt.png differ diff --git a/assets/character/Yanqing.png b/assets/character/Yanqing.png new file mode 100644 index 000000000..0f6b765b1 Binary files /dev/null and b/assets/character/Yanqing.png differ diff --git a/assets/character/Yukong.png b/assets/character/Yukong.png new file mode 100644 index 000000000..84b697760 Binary files /dev/null and b/assets/character/Yukong.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_ADD.png b/assets/share/combat/support/COMBAT_SUPPORT_ADD.png new file mode 100644 index 000000000..586c9e0e5 Binary files /dev/null and b/assets/share/combat/support/COMBAT_SUPPORT_ADD.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST.png new file mode 100644 index 000000000..d22e1c0d1 Binary files /dev/null and b/assets/share/combat/support/COMBAT_SUPPORT_LIST.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png new file mode 100644 index 000000000..3029cfbb8 Binary files /dev/null and b/assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png b/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png new file mode 100644 index 000000000..aea5bb872 Binary files /dev/null and b/assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png differ diff --git a/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png b/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png new file mode 100644 index 000000000..5ea0ab7bc Binary files /dev/null and b/assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png differ diff --git a/assets/share/combat/team/COMBAT_TEAM_DISMISSSUPPORT.png b/assets/share/combat/team/COMBAT_TEAM_DISMISSSUPPORT.png new file mode 100644 index 000000000..4a9ca1975 Binary files /dev/null and b/assets/share/combat/team/COMBAT_TEAM_DISMISSSUPPORT.png differ diff --git a/assets/share/combat/team/COMBAT_TEAM_SUPPORT.png b/assets/share/combat/team/COMBAT_TEAM_SUPPORT.png new file mode 100644 index 000000000..2c6f8e1bf Binary files /dev/null and b/assets/share/combat/team/COMBAT_TEAM_SUPPORT.png differ diff --git a/config/template.json b/config/template.json index f007b4ed2..fbe284d7d 100644 --- a/config/template.json +++ b/config/template.json @@ -42,7 +42,9 @@ "Dungeon": { "Name": "Calyx_Golden_Treasures", "NameAtDoubleCalyx": "Calyx_Golden_Treasures", - "Team": 1 + "Team": 1, + "Support": "when_daily", + "SupportCharacter": "FirstCharacter" } }, "DailyQuest": { diff --git a/dev_tools/keyword_extract.py b/dev_tools/keyword_extract.py index 544dd5a3d..4f81d4dfe 100644 --- a/dev_tools/keyword_extract.py +++ b/dev_tools/keyword_extract.py @@ -25,7 +25,16 @@ def dungeon_name(name: str) -> str: name = re.sub('Bud_of_(.*)', r'Calyx_Crimson_\1', name).replace('Calyx_Crimson_Calyx_Crimson_', 'Calyx_Crimson_') name = re.sub('Shape_of_(.*)', r'Stagnant_Shadow_\1', name) if name in ['Destructions_Beginning', 'End_of_the_Eternal_Freeze']: - name = 'Echo_of_War_' + name + name = f'Echo_of_War_{name}' + return name + + +nickname_count = 0 + + +def character_name(name: str) -> str: + name = text_to_variable(name) + name = re.sub('_', '', name) return name @@ -44,6 +53,7 @@ class TextMap: data = {} for id_, text in read_file(file).items(): text = text.replace('\u00A0', '') + text = text.replace(r'{NICKNAME}', 'Trailblazer') data[int(id_)] = text return data @@ -177,6 +187,18 @@ class KeywordExtract: quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash] self.load_keywords(quest_keywords, lang) + def load_character_name_keywords(self, lang='en'): + file_name = 'ItemConfigAvatarPlayerIcon.json' + path = os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', file_name) + character_data = read_file(path) + characters_hash = [character_data[key]["ItemName"]["Hash"] for key in character_data] + + text_map = self.text_map[lang] + keywords_id = sorted( + {text_map.find(keyword)[1] for keyword in characters_hash} + ) + self.load_keywords(keywords_id, lang) + def generate_forgotten_hall_stages(self): keyword_class = "ForgottenHallStage" output_file = './tasks/forgotten_hall/keywords/stage.py' @@ -230,6 +252,11 @@ class KeywordExtract: text_convert=text_convert(world), generator=gen) gen.write('./tasks/map/keywords/plane.py') + def generate_character_keywords(self): + self.load_character_name_keywords() + self.write_keywords(keyword_class='CharacterList', output_file='./tasks/character/keywords/character_list.py', + text_convert=character_name) + def generate(self): self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭']) self.write_keywords(keyword_class='DungeonNav', output_file='./tasks/dungeon/keywords/nav.py') @@ -249,6 +276,7 @@ class KeywordExtract: self.generate_assignment_keywords() self.generate_forgotten_hall_stages() self.generate_map_planes() + self.generate_character_keywords() self.load_keywords(['养成材料', '光锥', '遗器', '其他材料', '消耗品', '任务', '贵重物']) self.write_keywords(keyword_class='ItemTab', text_convert=lambda name: name.replace(' ', ''), output_file='./tasks/item/keywords/tab.py') diff --git a/module/config/argument/args.json b/module/config/argument/args.json index cf699b238..31c0db49b 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -227,6 +227,50 @@ 5, 6 ] + }, + "Support": { + "type": "select", + "value": "when_daily", + "option": [ + "do_not_use", + "always_use", + "when_daily" + ] + }, + "SupportCharacter": { + "type": "select", + "value": "FirstCharacter", + "option": [ + "FirstCharacter", + "Arlan", + "Asta", + "Bailu", + "Bronya", + "Clara", + "DanHeng", + "Gepard", + "Herta", + "Himeko", + "Hook", + "JingYuan", + "Kafka", + "Luocha", + "March7th", + "Natasha", + "Pela", + "Qingque", + "Sampo", + "Seele", + "Serval", + "SilverWolf", + "Sushang", + "Tingyun", + "TrailblazertheDestruction", + "TrailblazerthePreservation", + "Welt", + "Yanqing", + "Yukong" + ] } } }, diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 7de518cdf..8eb6ba1ce 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -81,6 +81,13 @@ Dungeon: Team: value: 1 option: [ 1, 2, 3, 4, 5, 6 ] + Support: + value: when_daily + option: [do_not_use, always_use, when_daily] + SupportCharacter: + # Options will be injected in config updater + value: FirstCharacter + option: [FirstCharacter, ] Assignment: Duration: diff --git a/module/config/config_generated.py b/module/config/config_generated.py index a09559864..49641de3b 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -42,6 +42,8 @@ class GeneratedConfig: Dungeon_Name = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility, Stagnant_Shadow_Quanta, Stagnant_Shadow_Gust, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Blaze, Stagnant_Shadow_Spike, Stagnant_Shadow_Rime, Stagnant_Shadow_Mirage, Stagnant_Shadow_Icicle, Stagnant_Shadow_Doom, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # do_not_participate, Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility Dungeon_Team = 1 # 1, 2, 3, 4, 5, 6 + Dungeon_Support = 'when_daily' # do_not_use, always_use, when_daily + Dungeon_SupportCharacter = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Bronya, Clara, DanHeng, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luocha, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazertheDestruction, TrailblazerthePreservation, Welt, Yanqing, Yukong # Group `Assignment` Assignment_Duration = 20 # 4, 8, 12, 20 diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 2e8d5c0f1..06843480a 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -1,11 +1,10 @@ -import re from copy import deepcopy from cached_property import cached_property from deploy.Windows.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write from module.base.timer import timer -from module.config.server import to_server, to_package, VALID_PACKAGE, VALID_CHANNEL_PACKAGE +from module.config.server import to_package, VALID_PACKAGE, VALID_CHANNEL_PACKAGE from module.config.utils import * CONFIG_IMPORT = ''' @@ -290,6 +289,16 @@ class ConfigGenerator: if value: deep_set(new, keys=['Dungeon', 'NameAtDoubleCalyx', dungeon], value=value) + from tasks.character.keywords import CharacterList + ingame_lang = gui_lang_to_ingame_lang(lang) + characters = deep_get(self.argument, keys='Dungeon.SupportCharacter.option') + for character in CharacterList.instances.values(): + if character.name in characters: + value = character.__getattribute__(ingame_lang) + if "Trailblazer" in value: + continue + deep_set(new, keys=['Dungeon', 'SupportCharacter', character.name], value=value) + # GUI i18n for path, _ in deep_iter(self.gui, depth=2): group, key = path @@ -362,6 +371,12 @@ class ConfigGenerator: dungeons = [dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_daily_dungeon] deep_set(self.argument, keys='Dungeon.Name.option', value=dungeons) deep_set(self.args, keys='Dungeon.Dungeon.Name.option', value=dungeons) + + from tasks.character.keywords import CharacterList + characters = ['FirstCharacter'] + [character.name for character in CharacterList.instances.values()] + deep_set(self.argument, keys='Dungeon.SupportCharacter.option', value=characters) + deep_set(self.args, keys='Dungeon.Dungeon.SupportCharacter.option', value=characters) + dungeons = deep_get(self.argument, keys='Dungeon.NameAtDoubleCalyx.option') dungeons += [dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson] diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index dd8b85086..5ce17cacb 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -233,6 +233,46 @@ "4": "4", "5": "5", "6": "6" + }, + "Support": { + "name": "Enable buddy support", + "help": "Whether to enable buddy support", + "do_not_use": "do_not_use", + "always_use": "always_use", + "when_daily": "when_daily" + }, + "SupportCharacter": { + "name": "Dungeon.SupportCharacter.name", + "help": "Dungeon.SupportCharacter.help", + "FirstCharacter": "FirstCharacter", + "Arlan": "Arlan", + "Asta": "Asta", + "Bailu": "Bailu", + "Bronya": "Bronya", + "Clara": "Clara", + "DanHeng": "Dan Heng", + "Gepard": "Gepard", + "Herta": "Herta", + "Himeko": "Himeko", + "Hook": "Hook", + "JingYuan": "Jing Yuan", + "Kafka": "Kafka", + "Luocha": "Luocha", + "March7th": "March 7th", + "Natasha": "Natasha", + "Pela": "Pela", + "Qingque": "Qingque", + "Sampo": "Sampo", + "Seele": "Seele", + "Serval": "Serval", + "SilverWolf": "Silver Wolf", + "Sushang": "Sushang", + "Tingyun": "Tingyun", + "TrailblazertheDestruction": "Trailblazer: the Destruction", + "TrailblazerthePreservation": "Trailblazer: the Preservation", + "Welt": "Welt", + "Yanqing": "Yanqing", + "Yukong": "Yukong" } }, "Assignment": { diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 4b4374fd0..a04d0f0d9 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -233,6 +233,46 @@ "4": "4", "5": "5", "6": "6" + }, + "Support": { + "name": "Dungeon.Support.name", + "help": "Dungeon.Support.help", + "do_not_use": "do_not_use", + "always_use": "always_use", + "when_daily": "when_daily" + }, + "SupportCharacter": { + "name": "Dungeon.SupportCharacter.name", + "help": "Dungeon.SupportCharacter.help", + "FirstCharacter": "FirstCharacter", + "Arlan": "アーラン", + "Asta": "アスター", + "Bailu": "白露", + "Bronya": "ブローニャ", + "Clara": "クラーラ", + "DanHeng": "丹恒", + "Gepard": "ジェパード", + "Herta": "ヘルタ", + "Himeko": "姫子", + "Hook": "フック", + "JingYuan": "景元", + "Kafka": "カフカ", + "Luocha": "羅刹", + "March7th": "三月なのか", + "Natasha": "ナターシャ", + "Pela": "ペラ", + "Qingque": "青雀", + "Sampo": "サンポ", + "Seele": "ゼーレ", + "Serval": "セーバル", + "SilverWolf": "銀狼", + "Sushang": "素裳", + "Tingyun": "停雲", + "TrailblazertheDestruction": "Trailblazer・壊滅", + "TrailblazerthePreservation": "Trailblazer・存護", + "Welt": "ヴェルト", + "Yanqing": "彦卿", + "Yukong": "御空" } }, "Assignment": { diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 5fb5f805a..f14bd1321 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -233,6 +233,46 @@ "4": "4", "5": "5", "6": "6" + }, + "Support": { + "name": "启用好友支援", + "help": "是否启用好友支援", + "do_not_use": "否", + "always_use": "是", + "when_daily": "仅当每日任务需要时" + }, + "SupportCharacter": { + "name": "好友支援角色", + "help": "选择好友支援角色,未找到则选择默认(第一个)角色", + "FirstCharacter": "支援列表第一个角色", + "Arlan": "阿兰", + "Asta": "艾丝妲", + "Bailu": "白露", + "Bronya": "布洛妮娅", + "Clara": "克拉拉", + "DanHeng": "丹恒", + "Gepard": "杰帕德", + "Herta": "黑塔", + "Himeko": "姬子", + "Hook": "虎克", + "JingYuan": "景元", + "Kafka": "卡芙卡(未实装)", + "Luocha": "罗刹", + "March7th": "三月七", + "Natasha": "娜塔莎", + "Pela": "佩拉", + "Qingque": "青雀", + "Sampo": "桑博", + "Seele": "希儿", + "Serval": "希露瓦", + "SilverWolf": "银狼", + "Sushang": "素裳", + "Tingyun": "停云", + "TrailblazertheDestruction": "开拓者•毁灭", + "TrailblazerthePreservation": "开拓者•存护", + "Welt": "瓦尔特", + "Yanqing": "彦卿", + "Yukong": "驭空" } }, "Assignment": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index ae51b8066..9c8ea4bb1 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -233,6 +233,46 @@ "4": "4", "5": "5", "6": "6" + }, + "Support": { + "name": "Dungeon.Support.name", + "help": "Dungeon.Support.help", + "do_not_use": "do_not_use", + "always_use": "always_use", + "when_daily": "when_daily" + }, + "SupportCharacter": { + "name": "Dungeon.SupportCharacter.name", + "help": "Dungeon.SupportCharacter.help", + "FirstCharacter": "FirstCharacter", + "Arlan": "阿蘭", + "Asta": "艾絲妲", + "Bailu": "白露", + "Bronya": "布洛妮婭", + "Clara": "克拉拉", + "DanHeng": "丹恆", + "Gepard": "傑帕德", + "Herta": "黑塔", + "Himeko": "姬子", + "Hook": "虎克", + "JingYuan": "景元", + "Kafka": "卡芙卡", + "Luocha": "羅剎", + "March7th": "三月七", + "Natasha": "娜塔莎", + "Pela": "佩拉", + "Qingque": "青雀", + "Sampo": "桑博", + "Seele": "希兒", + "Serval": "希露瓦", + "SilverWolf": "銀狼", + "Sushang": "素裳", + "Tingyun": "停雲", + "TrailblazertheDestruction": "Trailblazer•毀滅", + "TrailblazerthePreservation": "Trailblazer•存護", + "Welt": "瓦爾特", + "Yanqing": "彥卿", + "Yukong": "馭空" } }, "Assignment": { diff --git a/tasks/character/keywords/__init__.py b/tasks/character/keywords/__init__.py new file mode 100644 index 000000000..83cecbfaf --- /dev/null +++ b/tasks/character/keywords/__init__.py @@ -0,0 +1,2 @@ +import tasks.character.keywords.character_list as KEYWORD_CHARACTER_LIST +from tasks.character.keywords.classes import CharacterList \ No newline at end of file diff --git a/tasks/character/keywords/character_list.py b/tasks/character/keywords/character_list.py new file mode 100644 index 000000000..68e682579 --- /dev/null +++ b/tasks/character/keywords/character_list.py @@ -0,0 +1,229 @@ +from .classes import CharacterList + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.keyword_extract ``` + +Arlan = CharacterList( + id=1, + name='Arlan', + cn='阿兰', + cht='阿蘭', + en='Arlan', + jp='アーラン', +) +Asta = CharacterList( + id=2, + name='Asta', + cn='艾丝妲', + cht='艾絲妲', + en='Asta', + jp='アスター', +) +Bailu = CharacterList( + id=3, + name='Bailu', + cn='白露', + cht='白露', + en='Bailu', + jp='白露', +) +Bronya = CharacterList( + id=4, + name='Bronya', + cn='布洛妮娅', + cht='布洛妮婭', + en='Bronya', + jp='ブローニャ', +) +Clara = CharacterList( + id=5, + name='Clara', + cn='克拉拉', + cht='克拉拉', + en='Clara', + jp='クラーラ', +) +DanHeng = CharacterList( + id=6, + name='DanHeng', + cn='丹恒', + cht='丹恆', + en='Dan Heng', + jp='丹恒', +) +Gepard = CharacterList( + id=7, + name='Gepard', + cn='杰帕德', + cht='傑帕德', + en='Gepard', + jp='ジェパード', +) +Herta = CharacterList( + id=8, + name='Herta', + cn='黑塔', + cht='黑塔', + en='Herta', + jp='ヘルタ', +) +Himeko = CharacterList( + id=9, + name='Himeko', + cn='姬子', + cht='姬子', + en='Himeko', + jp='姫子', +) +Hook = CharacterList( + id=10, + name='Hook', + cn='虎克', + cht='虎克', + en='Hook', + jp='フック', +) +JingYuan = CharacterList( + id=11, + name='JingYuan', + cn='景元', + cht='景元', + en='Jing Yuan', + jp='景元', +) +Kafka = CharacterList( + id=12, + name='Kafka', + cn='卡芙卡', + cht='卡芙卡', + en='Kafka', + jp='カフカ', +) +Luocha = CharacterList( + id=13, + name='Luocha', + cn='罗刹', + cht='羅剎', + en='Luocha', + jp='羅刹', +) +March7th = CharacterList( + id=14, + name='March7th', + cn='三月七', + cht='三月七', + en='March 7th', + jp='三月なのか', +) +Natasha = CharacterList( + id=15, + name='Natasha', + cn='娜塔莎', + cht='娜塔莎', + en='Natasha', + jp='ナターシャ', +) +Pela = CharacterList( + id=16, + name='Pela', + cn='佩拉', + cht='佩拉', + en='Pela', + jp='ペラ', +) +Qingque = CharacterList( + id=17, + name='Qingque', + cn='青雀', + cht='青雀', + en='Qingque', + jp='青雀', +) +Sampo = CharacterList( + id=18, + name='Sampo', + cn='桑博', + cht='桑博', + en='Sampo', + jp='サンポ', +) +Seele = CharacterList( + id=19, + name='Seele', + cn='希儿', + cht='希兒', + en='Seele', + jp='ゼーレ', +) +Serval = CharacterList( + id=20, + name='Serval', + cn='希露瓦', + cht='希露瓦', + en='Serval', + jp='セーバル', +) +SilverWolf = CharacterList( + id=21, + name='SilverWolf', + cn='银狼', + cht='銀狼', + en='Silver Wolf', + jp='銀狼', +) +Sushang = CharacterList( + id=22, + name='Sushang', + cn='素裳', + cht='素裳', + en='Sushang', + jp='素裳', +) +Tingyun = CharacterList( + id=23, + name='Tingyun', + cn='停云', + cht='停雲', + en='Tingyun', + jp='停雲', +) +TrailblazertheDestruction = CharacterList( + id=24, + name='TrailblazertheDestruction', + cn='Trailblazer•毁灭', + cht='Trailblazer•毀滅', + en='Trailblazer: the Destruction', + jp='Trailblazer・壊滅', +) +TrailblazerthePreservation = CharacterList( + id=25, + name='TrailblazerthePreservation', + cn='Trailblazer•存护', + cht='Trailblazer•存護', + en='Trailblazer: the Preservation', + jp='Trailblazer・存護', +) +Welt = CharacterList( + id=26, + name='Welt', + cn='瓦尔特', + cht='瓦爾特', + en='Welt', + jp='ヴェルト', +) +Yanqing = CharacterList( + id=27, + name='Yanqing', + cn='彦卿', + cht='彥卿', + en='Yanqing', + jp='彦卿', +) +Yukong = CharacterList( + id=28, + name='Yukong', + cn='驭空', + cht='馭空', + en='Yukong', + jp='御空', +) diff --git a/tasks/character/keywords/classes.py b/tasks/character/keywords/classes.py new file mode 100644 index 000000000..6bd84c3d6 --- /dev/null +++ b/tasks/character/keywords/classes.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import ClassVar + +from module.ocr.keyword import Keyword + +@dataclass(repr=False) +class CharacterList(Keyword): + instances: ClassVar = {} \ No newline at end of file diff --git a/tasks/combat/assets/assets_combat_support.py b/tasks/combat/assets/assets_combat_support.py new file mode 100644 index 000000000..c5a3d4ecd --- /dev/null +++ b/tasks/combat/assets/assets_combat_support.py @@ -0,0 +1,55 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +COMBAT_SUPPORT_ADD = ButtonWrapper( + name='COMBAT_SUPPORT_ADD', + share=Button( + file='./assets/share/combat/support/COMBAT_SUPPORT_ADD.png', + area=(1032, 649, 1132, 680), + search=(1012, 629, 1152, 700), + color=(228, 228, 228), + button=(1032, 649, 1132, 680), + ), +) +COMBAT_SUPPORT_LIST = ButtonWrapper( + name='COMBAT_SUPPORT_LIST', + share=Button( + file='./assets/share/combat/support/COMBAT_SUPPORT_LIST.png', + area=(57, 637, 100, 680), + search=(37, 617, 120, 700), + color=(212, 213, 215), + button=(57, 637, 100, 680), + ), +) +COMBAT_SUPPORT_LIST_GRID = ButtonWrapper( + name='COMBAT_SUPPORT_LIST_GRID', + share=Button( + file='./assets/share/combat/support/COMBAT_SUPPORT_LIST_GRID.png', + area=(64, 115, 159, 634), + search=(44, 95, 179, 654), + color=(119, 108, 132), + button=(64, 115, 159, 634), + ), +) +COMBAT_SUPPORT_LIST_SCROLL = ButtonWrapper( + name='COMBAT_SUPPORT_LIST_SCROLL', + share=Button( + file='./assets/share/combat/support/COMBAT_SUPPORT_LIST_SCROLL.png', + area=(448, 112, 452, 610), + search=(428, 92, 472, 630), + color=(127, 133, 150), + button=(448, 112, 452, 610), + ), +) +COMBAT_SUPPORT_SELECTED = ButtonWrapper( + name='COMBAT_SUPPORT_SELECTED', + share=Button( + file='./assets/share/combat/support/COMBAT_SUPPORT_SELECTED.png', + area=(69, 114, 91, 116), + search=(49, 94, 111, 136), + color=(254, 254, 254), + button=(69, 114, 91, 116), + ), +) diff --git a/tasks/combat/assets/assets_combat_team.py b/tasks/combat/assets/assets_combat_team.py index 9adce955d..30413e998 100644 --- a/tasks/combat/assets/assets_combat_team.py +++ b/tasks/combat/assets/assets_combat_team.py @@ -3,6 +3,16 @@ from module.base.button import Button, ButtonWrapper # This file was auto-generated, do not modify it manually. To generate: # ``` python -m dev_tools.button_extract ``` +COMBAT_TEAM_DISMISSSUPPORT = ButtonWrapper( + name='COMBAT_TEAM_DISMISSSUPPORT', + share=Button( + file='./assets/share/combat/team/COMBAT_TEAM_DISMISSSUPPORT.png', + area=(1127, 477, 1154, 501), + search=(1107, 457, 1174, 521), + color=(132, 140, 150), + button=(1127, 477, 1154, 501), + ), +) COMBAT_TEAM_PREPARE = ButtonWrapper( name='COMBAT_TEAM_PREPARE', cn=Button( @@ -13,6 +23,16 @@ COMBAT_TEAM_PREPARE = ButtonWrapper( button=(958, 641, 1193, 676), ), ) +COMBAT_TEAM_SUPPORT = ButtonWrapper( + name='COMBAT_TEAM_SUPPORT', + share=Button( + file='./assets/share/combat/team/COMBAT_TEAM_SUPPORT.png', + area=(1123, 477, 1158, 503), + search=(1103, 457, 1178, 523), + color=(195, 215, 201), + button=(1123, 477, 1158, 503), + ), +) TEAM_1 = ButtonWrapper( name='TEAM_1', share=Button( diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index 4e4931ae6..44115f075 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -3,15 +3,17 @@ from module.logger import logger from tasks.base.assets.assets_base_page import CLOSE from tasks.combat.assets.assets_combat_finish import COMBAT_AGAIN, COMBAT_EXIT from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE -from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_PREPARE +from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_PREPARE, COMBAT_TEAM_SUPPORT, COMBAT_TEAM_DISMISSSUPPORT +from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST from tasks.combat.interact import CombatInteract from tasks.combat.prepare import CombatPrepare from tasks.combat.state import CombatState from tasks.combat.team import CombatTeam +from tasks.combat.support import CombatSupport from tasks.map.control.joystick import MapControlJoystick -class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJoystick): +class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSupport, MapControlJoystick): def handle_combat_prepare(self): """ Returns: @@ -63,11 +65,13 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ return False - def combat_prepare(self, team=1): + def combat_prepare(self, team=1, support_character: str = None): """ Args: team: 1 to 6. - + skip_first_screenshot: + support_character: Support character name + Returns: bool: True if success to enter combat False if trialblaze power is not enough @@ -78,6 +82,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ """ logger.hr('Combat prepare') skip_first_screenshot = True + pre_set_team = bool(support_character) while 1: if skip_first_screenshot: skip_first_screenshot = False @@ -89,6 +94,13 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ return True # Click + if self.appear(COMBAT_TEAM_SUPPORT) and support_character: + if pre_set_team: + self.team_set(team) + pre_set_team = False + continue + self.support_set(support_character) + continue if self.appear(COMBAT_TEAM_PREPARE, interval=2): self.team_set(team) self.device.click(COMBAT_TEAM_PREPARE) @@ -260,7 +272,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ self.device.click(COMBAT_EXIT) continue - def combat(self, team: int = 1, wave_limit: int = 0, skip_first_screenshot=True): + def combat(self, team: int = 1, wave_limit: int = 0, skip_first_screenshot=True, support_character: str = None): """ Combat until trailblaze power runs out. @@ -268,6 +280,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ team: 1 to 6. wave_limit: Limit combat runs, 0 means no limit. skip_first_screenshot: + use_support: "do_not_use", "always_use", "when_daily" + is_daily: True if is a daily task + support_character: Support character name Returns: bool: True if trailblaze power exhausted @@ -287,7 +302,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ logger.hr('Combat', level=2) logger.info(f'Combat, team={team}, wave={self.combat_wave_done}/{self.combat_wave_limit}') # Prepare - prepare = self.combat_prepare(team) + prepare = self.combat_prepare(team, support_character) if not prepare: self.combat_exit() break diff --git a/tasks/combat/support.py b/tasks/combat/support.py new file mode 100644 index 000000000..cc7489238 --- /dev/null +++ b/tasks/combat/support.py @@ -0,0 +1,217 @@ +import cv2 +import numpy as np +from scipy import signal +from module.base.timer import Timer +from module.base.utils import area_size, crop, rgb2luma, load_image +from module.logger import logger +from module.ui.scroll import Scroll +from tasks.base.ui import UI +from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST, \ + COMBAT_SUPPORT_LIST_SCROLL, COMBAT_SUPPORT_SELECTED +from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_SUPPORT, COMBAT_TEAM_DISMISSSUPPORT + + +class SupportCharacter: + _image_cache = {} + + def __init__(self, name, screenshot, similarity=0.85): + self.name = name + self.image = self._scale_character() + self.screenshot = screenshot + self.similarity = similarity + self.button = self._find_character() + + def __bool__(self): + # __bool__ is called when use an object of the class in a boolean context + return self.button is not None + + def _scale_character(self): + """ + Returns: + Image: Character image after scaled + """ + + if self.name in SupportCharacter._image_cache: + logger.info(f"Using cached image of {self.name}") + return SupportCharacter._image_cache[self.name] + + img = load_image(f"assets/character/{self.name}.png") + scaled_img = cv2.resize(img, (85, 82)) + SupportCharacter._image_cache[self.name] = scaled_img + logger.info(f"Character {self.name} image cached") + return scaled_img + + def _find_character(self): + character = np.array(self.image) + support_list_img = self.screenshot + res = cv2.matchTemplate(character, support_list_img, cv2.TM_CCOEFF_NORMED) + + _, max_val, _, max_loc = cv2.minMaxLoc(res) + + character_width = character.shape[1] + character_height = character.shape[0] + + return (max_loc[0], max_loc[1], max_loc[0] + character_width, max_loc[1] + character_height) \ + if max_val >= self.similarity else None + + def selected_icon_search(self): + """ + Returns: + tuple: (x1, y1, x2, y2) of selected icon search area + """ + return ( + self.button[0], self.button[1] - 5, self.button[0] + 30, self.button[1]) if self.button else None + + +class SupportListScroll(Scroll): + def cal_position(self, main): + """ + Args: + main (ModuleBase): + + Returns: + float: 0 to 1. + """ + image = main.device.image + + temp_area = list(self.area) + temp_area[0] = int(temp_area[0] * 0.98) + temp_area[2] = int(temp_area[2] * 1.02) + + line = rgb2luma(crop(image, temp_area)).flatten() + width = area_size(temp_area)[0] + parameters = { + "height": 180, + "prominence": 30, + "distance": width * 0.75, + } + peaks, _ = signal.find_peaks(line, **parameters) + peaks //= width + self.length = len(peaks) + middle = np.mean(peaks) + + position = (middle - self.length / 2) / (self.total - self.length) + position = position if position > 0 else 0.0 + position = position if position < 1 else 1.0 + logger.attr( + self.name, f"{position:.2f} ({middle}-{self.length / 2})/({self.total}-{self.length})") + return position + + +class CombatSupport(UI): + def support_set(self, support_character_name: str = "FirstCharacter"): + """ + Args: + support_character_name: Support character name + + Returns: + bool: If clicked + + Pages: + in: COMBAT_PREPARE + mid: COMBAT_SUPPORT_LIST + out: COMBAT_PREPARE + """ + logger.hr("Combat support") + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(COMBAT_TEAM_DISMISSSUPPORT): + return True + + # Click + if self.appear(COMBAT_TEAM_SUPPORT, interval=2): + self.device.click(COMBAT_TEAM_SUPPORT) + self.interval_reset(COMBAT_TEAM_SUPPORT) + continue + if self.appear(COMBAT_SUPPORT_LIST, interval=2): + if support_character_name != "FirstCharacter": + self._search_support( + support_character_name) # Search support + self.device.click(COMBAT_SUPPORT_ADD) + self.interval_reset(COMBAT_SUPPORT_LIST) + continue + + def _search_support(self, support_character_name: str = "JingYuan"): + """ + Args: + support_character_name: Support character name + + Returns: + bool: True if found support else False + + Pages: + in: COMBAT_SUPPORT_LIST + out: COMBAT_SUPPORT_LIST + """ + logger.hr("Combat support search") + scroll = SupportListScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, color=(194, 196, 205), + name=COMBAT_SUPPORT_LIST_SCROLL.name) + if scroll.appear(main=self): + if not scroll.at_bottom(main=self): + scroll.set_bottom(main=self) + scroll.set_top(main=self) + + logger.info("Searching support") + skip_first_screenshot = False + character = None + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if not support_character_name.startswith("Trailblazer"): + character = SupportCharacter(support_character_name, self.device.image) + else: + character = SupportCharacter(f"Stelle{support_character_name[11:]}", + self.device.image) or SupportCharacter( + f"Caelum{support_character_name[11:]}", self.device.image) + + if character: + logger.info("Support found") + if self._select_support(character): + return True + else: + logger.warning("Support not selected") + return False + + if not scroll.at_bottom(main=self): + scroll.next_page(main=self) + continue + else: + logger.info("Support not found") + return False + + def _select_support(self, character: SupportCharacter): + """ + Args: + character: Support character + + Pages: + in: COMBAT_SUPPORT_LIST + out: COMBAT_SUPPORT_LIST + """ + logger.hr("Combat support select") + COMBAT_SUPPORT_SELECTED.matched_button.search = character.selected_icon_search() + skip_first_screenshot = False + interval = Timer(2) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.match_template(COMBAT_SUPPORT_SELECTED): + return True + + if interval.reached(): + self.device.click(character) + interval.reset() + continue diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index 956c1cc07..0e703b685 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -7,11 +7,16 @@ from tasks.dungeon.ui import DungeonUI class Dungeon(DungeonUI, DungeonEvent, Combat): - def run(self, dungeon: DungeonList = None, team: int = None): + def run(self, dungeon: DungeonList = None, team: int = None, use_support: str = None, is_daily: bool = False, + support_character: str = None): if dungeon is None: dungeon = DungeonList.find(self.config.Dungeon_Name) if team is None: team = self.config.Dungeon_Team + if use_support is None: + use_support = self.config.Dungeon_Support + if support_character is None: + support_character = self.config.Dungeon_SupportCharacter if use_support == "always_use" or use_support == "when_daily" and is_daily else None # UI switches switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) @@ -26,7 +31,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): self._dungeon_nav_goto(calyx) if remain := self.get_double_event_remain(): self.dungeon_goto(calyx) - if self.combat(team, wave_limit=remain): + if self.combat(team, wave_limit=remain, support_character=support_character): self.delay_dungeon_task(calyx) self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) @@ -38,7 +43,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) self.dungeon_goto(dungeon) - self.combat(team) + self.combat(team=team, support_character=support_character) self.delay_dungeon_task(dungeon) def delay_dungeon_task(self, dungeon):