Add: character and combat support (#38)

* Add: combat support

* Fix: team_set logic when using support in combat

* Add: Support character selection

* Upd: optimize support character get logic

* Fix: bug in get_character_by_name

* Upd: use keywords to generate support config

* Fix: Correct comment language to English

* Fix: Remove debug statements

* Upd: Improve UI icons and positioning

* Upd: Refactor support

* Upd: character keyword extract

* Upd:Character icon

* Upd: Character icon

* Upd: Character icon

* Upd: Optimize the parameter in SupportListScroll

* Upd: Refactor support

* Fix: Bug in Dungeon

* Fix: Corrected comments in Combat and Support

* Upd: Modified parameter

* Refactor: Support logic

* Refactor: Support logic

* Refactor: Support logic

* Upd: Character icon

* Upd: Character icon

* Refactor: Support logic

* Refactor: Support logic
This commit is contained in:
X-Zero-L 2023-07-24 18:15:22 +08:00 committed by GitHub
parent 0bd94ebecc
commit b2ab868351
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 822 additions and 13 deletions

BIN
assets/character/Arlan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Asta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Bailu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/character/Bronya.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/character/Clara.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/character/Gepard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/character/Herta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Himeko.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/character/Hook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/character/Luocha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Pela.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/character/Sampo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Seele.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/character/Serval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/character/Welt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/character/Yukong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -42,7 +42,9 @@
"Dungeon": { "Dungeon": {
"Name": "Calyx_Golden_Treasures", "Name": "Calyx_Golden_Treasures",
"NameAtDoubleCalyx": "Calyx_Golden_Treasures", "NameAtDoubleCalyx": "Calyx_Golden_Treasures",
"Team": 1 "Team": 1,
"Support": "when_daily",
"SupportCharacter": "FirstCharacter"
} }
}, },
"DailyQuest": { "DailyQuest": {

View File

@ -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('Bud_of_(.*)', r'Calyx_Crimson_\1', name).replace('Calyx_Crimson_Calyx_Crimson_', 'Calyx_Crimson_')
name = re.sub('Shape_of_(.*)', r'Stagnant_Shadow_\1', name) name = re.sub('Shape_of_(.*)', r'Stagnant_Shadow_\1', name)
if name in ['Destructions_Beginning', 'End_of_the_Eternal_Freeze']: 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 return name
@ -44,6 +53,7 @@ class TextMap:
data = {} data = {}
for id_, text in read_file(file).items(): for id_, text in read_file(file).items():
text = text.replace('\u00A0', '') text = text.replace('\u00A0', '')
text = text.replace(r'{NICKNAME}', 'Trailblazer')
data[int(id_)] = text data[int(id_)] = text
return data return data
@ -177,6 +187,18 @@ class KeywordExtract:
quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash] quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash]
self.load_keywords(quest_keywords, lang) 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): def generate_forgotten_hall_stages(self):
keyword_class = "ForgottenHallStage" keyword_class = "ForgottenHallStage"
output_file = './tasks/forgotten_hall/keywords/stage.py' output_file = './tasks/forgotten_hall/keywords/stage.py'
@ -230,6 +252,11 @@ class KeywordExtract:
text_convert=text_convert(world), generator=gen) text_convert=text_convert(world), generator=gen)
gen.write('./tasks/map/keywords/plane.py') 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): def generate(self):
self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭']) self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭'])
self.write_keywords(keyword_class='DungeonNav', output_file='./tasks/dungeon/keywords/nav.py') 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_assignment_keywords()
self.generate_forgotten_hall_stages() self.generate_forgotten_hall_stages()
self.generate_map_planes() self.generate_map_planes()
self.generate_character_keywords()
self.load_keywords(['养成材料', '光锥', '遗器', '其他材料', '消耗品', '任务', '贵重物']) self.load_keywords(['养成材料', '光锥', '遗器', '其他材料', '消耗品', '任务', '贵重物'])
self.write_keywords(keyword_class='ItemTab', text_convert=lambda name: name.replace(' ', ''), self.write_keywords(keyword_class='ItemTab', text_convert=lambda name: name.replace(' ', ''),
output_file='./tasks/item/keywords/tab.py') output_file='./tasks/item/keywords/tab.py')

View File

@ -227,6 +227,50 @@
5, 5,
6 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"
]
} }
} }
}, },

View File

@ -81,6 +81,13 @@ Dungeon:
Team: Team:
value: 1 value: 1
option: [ 1, 2, 3, 4, 5, 6 ] 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: Assignment:
Duration: Duration:

View File

@ -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_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_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_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` # Group `Assignment`
Assignment_Duration = 20 # 4, 8, 12, 20 Assignment_Duration = 20 # 4, 8, 12, 20

View File

@ -1,11 +1,10 @@
import re
from copy import deepcopy from copy import deepcopy
from cached_property import cached_property from cached_property import cached_property
from deploy.Windows.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write from deploy.Windows.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write
from module.base.timer import timer 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 * from module.config.utils import *
CONFIG_IMPORT = ''' CONFIG_IMPORT = '''
@ -290,6 +289,16 @@ class ConfigGenerator:
if value: if value:
deep_set(new, keys=['Dungeon', 'NameAtDoubleCalyx', dungeon], value=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 # GUI i18n
for path, _ in deep_iter(self.gui, depth=2): for path, _ in deep_iter(self.gui, depth=2):
group, key = path group, key = path
@ -362,6 +371,12 @@ class ConfigGenerator:
dungeons = [dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_daily_dungeon] 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.argument, keys='Dungeon.Name.option', value=dungeons)
deep_set(self.args, keys='Dungeon.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 = deep_get(self.argument, keys='Dungeon.NameAtDoubleCalyx.option')
dungeons += [dungeon.name for dungeon in DungeonList.instances.values() dungeons += [dungeon.name for dungeon in DungeonList.instances.values()
if dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson] if dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson]

View File

@ -233,6 +233,46 @@
"4": "4", "4": "4",
"5": "5", "5": "5",
"6": "6" "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": { "Assignment": {

View File

@ -233,6 +233,46 @@
"4": "4", "4": "4",
"5": "5", "5": "5",
"6": "6" "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": { "Assignment": {

View File

@ -233,6 +233,46 @@
"4": "4", "4": "4",
"5": "5", "5": "5",
"6": "6" "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": { "Assignment": {

View File

@ -233,6 +233,46 @@
"4": "4", "4": "4",
"5": "5", "5": "5",
"6": "6" "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": { "Assignment": {

View File

@ -0,0 +1,2 @@
import tasks.character.keywords.character_list as KEYWORD_CHARACTER_LIST
from tasks.character.keywords.classes import CharacterList

View File

@ -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='御空',
)

View File

@ -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 = {}

View File

@ -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),
),
)

View File

@ -3,6 +3,16 @@ from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate: # This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ``` # ``` 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( COMBAT_TEAM_PREPARE = ButtonWrapper(
name='COMBAT_TEAM_PREPARE', name='COMBAT_TEAM_PREPARE',
cn=Button( cn=Button(
@ -13,6 +23,16 @@ COMBAT_TEAM_PREPARE = ButtonWrapper(
button=(958, 641, 1193, 676), 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( TEAM_1 = ButtonWrapper(
name='TEAM_1', name='TEAM_1',
share=Button( share=Button(

View File

@ -3,15 +3,17 @@ from module.logger import logger
from tasks.base.assets.assets_base_page import CLOSE 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_finish import COMBAT_AGAIN, COMBAT_EXIT
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE 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.interact import CombatInteract
from tasks.combat.prepare import CombatPrepare from tasks.combat.prepare import CombatPrepare
from tasks.combat.state import CombatState from tasks.combat.state import CombatState
from tasks.combat.team import CombatTeam from tasks.combat.team import CombatTeam
from tasks.combat.support import CombatSupport
from tasks.map.control.joystick import MapControlJoystick 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): def handle_combat_prepare(self):
""" """
Returns: Returns:
@ -63,10 +65,12 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
return False return False
def combat_prepare(self, team=1): def combat_prepare(self, team=1, support_character: str = None):
""" """
Args: Args:
team: 1 to 6. team: 1 to 6.
skip_first_screenshot:
support_character: Support character name
Returns: Returns:
bool: True if success to enter combat bool: True if success to enter combat
@ -78,6 +82,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
""" """
logger.hr('Combat prepare') logger.hr('Combat prepare')
skip_first_screenshot = True skip_first_screenshot = True
pre_set_team = bool(support_character)
while 1: while 1:
if skip_first_screenshot: if skip_first_screenshot:
skip_first_screenshot = False skip_first_screenshot = False
@ -89,6 +94,13 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
return True return True
# Click # 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): if self.appear(COMBAT_TEAM_PREPARE, interval=2):
self.team_set(team) self.team_set(team)
self.device.click(COMBAT_TEAM_PREPARE) self.device.click(COMBAT_TEAM_PREPARE)
@ -260,7 +272,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
self.device.click(COMBAT_EXIT) self.device.click(COMBAT_EXIT)
continue 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. Combat until trailblaze power runs out.
@ -268,6 +280,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
team: 1 to 6. team: 1 to 6.
wave_limit: Limit combat runs, 0 means no limit. wave_limit: Limit combat runs, 0 means no limit.
skip_first_screenshot: 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: Returns:
bool: True if trailblaze power exhausted bool: True if trailblaze power exhausted
@ -287,7 +302,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, MapControlJ
logger.hr('Combat', level=2) logger.hr('Combat', level=2)
logger.info(f'Combat, team={team}, wave={self.combat_wave_done}/{self.combat_wave_limit}') logger.info(f'Combat, team={team}, wave={self.combat_wave_done}/{self.combat_wave_limit}')
# Prepare # Prepare
prepare = self.combat_prepare(team) prepare = self.combat_prepare(team, support_character)
if not prepare: if not prepare:
self.combat_exit() self.combat_exit()
break break

217
tasks/combat/support.py Normal file
View File

@ -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

View File

@ -7,11 +7,16 @@ from tasks.dungeon.ui import DungeonUI
class Dungeon(DungeonUI, DungeonEvent, Combat): 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: if dungeon is None:
dungeon = DungeonList.find(self.config.Dungeon_Name) dungeon = DungeonList.find(self.config.Dungeon_Name)
if team is None: if team is None:
team = self.config.Dungeon_Team 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 # UI switches
switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
@ -26,7 +31,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
self._dungeon_nav_goto(calyx) self._dungeon_nav_goto(calyx)
if remain := self.get_double_event_remain(): if remain := self.get_double_event_remain():
self.dungeon_goto(calyx) 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.delay_dungeon_task(calyx)
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) 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_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon) self.dungeon_goto(dungeon)
self.combat(team) self.combat(team=team, support_character=support_character)
self.delay_dungeon_task(dungeon) self.delay_dungeon_task(dungeon)
def delay_dungeon_task(self, dungeon): def delay_dungeon_task(self, dungeon):