diff --git a/model/avatar/_avatar.py b/model/avatar/_avatar.py index 1197d2c..dac47f7 100644 --- a/model/avatar/_avatar.py +++ b/model/avatar/_avatar.py @@ -1,12 +1,8 @@ -from typing import TYPE_CHECKING - +from model.avatar._talente import AvatarTalents from model.enums import Association, AvatarQuality, Element, WeaponType from model.other import ItemCount from utils.model import BaseModel -if TYPE_CHECKING: - from model.avatar._talente import AvatarTalents - __all__ = ( "Avatar", "AvatarBirth", @@ -74,17 +70,17 @@ class AvatarStories(BaseModel): story_5: Story """角色故事5""" - miscellaneous: Story + miscellaneous: Story | None = None """角色杂谈""" - vision: Story + vision: Story | None = None """神之眼""" class AvatarInfo(BaseModel): - title: str + title: str | None """称号""" - birth: AvatarBirth + birth: AvatarBirth | None """生日""" occupation: str """所属""" @@ -123,6 +119,8 @@ class AvatarConstellation(BaseModel): """命座描述""" icon: str """命座图标""" + param_list: list[float] + """命座数据参数列表""" class AvatarAttribute(BaseModel): @@ -177,7 +175,7 @@ class Avatar(BaseModel): """角色信息""" attributes: AvatarAttribute """角色基础属性""" - talents: "AvatarTalents" + talents: AvatarTalents """角色天赋信息""" promotes: list[AvatarPromote] """角色突破数据""" diff --git a/model/avatar/_talente.py b/model/avatar/_talente.py index 92e221e..21f85bf 100644 --- a/model/avatar/_talente.py +++ b/model/avatar/_talente.py @@ -112,16 +112,16 @@ class AvatarTalents(BaseModel): normal_attack: NormalAttack """普通攻击""" - elemental_skill: ElementalSkill + elemental_skill: ElementalSkill | None """元素战技""" - elemental_burst: ElementalBurst + elemental_burst: ElementalBurst | None """元素爆发""" alternate_sprint: AlternateSprint | None = None """冲刺技能""" - first_ascension_passive: FirstAscensionPassive + first_ascension_passive: FirstAscensionPassive | None """第一次突破固有天赋""" - fourth_ascension_passive: FourthAscensionPassive + fourth_ascension_passive: FourthAscensionPassive | None """第四次突破固有天赋""" utility_passive: UtilityPassive | None = None """实用固有天赋""" diff --git a/scripts/avatar.py b/scripts/avatar.py index 8a20c3f..c4a21dc 100644 --- a/scripts/avatar.py +++ b/scripts/avatar.py @@ -1,10 +1,15 @@ from itertools import chain from typing import TypeVar +import ujson as json +from aiofiles import open as async_open + from model.avatar import ( AlternateSprint, + Avatar, AvatarAttribute, AvatarBirth, + AvatarConstellation, AvatarInfo, AvatarPromote, AvatarStories, @@ -26,6 +31,7 @@ from model.avatar import ( from model.enums import Association, AvatarQuality, Element, WeaponType from model.other import ItemCount from utils.const import PROJECT_ROOT +from utils.funcs import remove_rich_tag from utils.manager import ResourceManager from utils.typedefs import Lang @@ -38,20 +44,184 @@ TalentType = TypeVar("TalentType", bound=Talent) OUT_DIR = PROJECT_ROOT.joinpath("out") -elements = { - 3057990932: Element.Cryo, - 467004516: Element.Anemo, - 821712868: Element.Null, - 2480172868: Element.Electro, - 4022324356: Element.Hydro, - 627825788: Element.Pyro, - 2596397668: Element.Dendro, - 967031460: Element.Geo, +elements_map = { + ( + 230082676, + 313529204, + 627825788, + 1247335084, + 1646245548, + 1740638908, + 3105283268, + 3112476852, + 3177381772, + 3847511308, + ): Element.Pyro, + ( + 321258364, + 483165900, + 756679372, + 1688473500, + 2480954540, + 3228108484, + 3400532572, + 3646588372, + 4022324356, + ): Element.Hydro, + ( + 126875444, + 467004516, + 550531300, + 898621369, + 1778251796, + 2075460644, + 2477900860, + 2648184060, + ): Element.Anemo, + ( + 122554396, + 608089036, + 689445588, + 1072755468, + 1821644548, + 1843086100, + 2085306033, + 2143937940, + 2480172868, + 2689029804, + 3352621156, + 4219874220, + ): Element.Electro, + (2161032364, 4017448612): Element.Dendro, + ( + 98482612, + 766902996, + 862088588, + 1480674860, + 1695600284, + 2778487532, + 2809830820, + 3057990932, + 4127670180, + 4220569804, + ): Element.Cryo, + ( + 825986772, + 967031460, + 1016213980, + 1662907292, + 2507042785, + 3219124204, + 3617274620, + 3929787020, + ): Element.Geo, + (471154292, 821712868, 1128382182, 3053155130, 4168416172): Element.Null, } -# noinspection PyShadowingBuiltins,SpellCheckingInspection +def get_skill_attributes(proud_skill_group_id: int) -> list[TalentAttribute]: + proud_skill_datas = sorted( + filter( + lambda x: x["proudSkillGroupId"] == proud_skill_group_id, + proud_skill_json_data, + ), + key=lambda x: x["level"], + ) + result: list[TalentAttribute] = [] + for proud_skill_data in proud_skill_datas: + param_descriptions = list( + filter( + lambda x: x is not None, + map( + lambda x: manager.get_text(x), + proud_skill_data["paramDescList"], + ), + ) + ) + param_num = len(param_descriptions) + for param_description in param_descriptions: + param_num = max( + param_num, + *map(int, re.findall(r"param(\d*)\:", param_description)), + ) + result.append( + TalentAttribute( + level=proud_skill_data["level"], + param_descriptions=param_descriptions, + param_list=proud_skill_data["paramList"][:param_num], + break_level=proud_skill_data.get("breakLevel", 0), + coin=proud_skill_data.get("coinCost", 0), + cost_items=list( + map( + lambda x: ItemCount(item_id=x["id"], count=x["count"]), + filter(lambda x: x, proud_skill_data["costItems"]), + ) + ), + ) + ) + return result + + +def parse_skill(skill_id: int, skill_cls: type[CombatTalent]) -> CombatTalent: + skill_data = next(filter(lambda x: x["id"] == skill_id, skill_json_data)) + _name = manager.get_text(skill_data["nameTextMapHash"]) + _description = manager.get_text(skill_data["descTextMapHash"]) + icon = skill_data["skillIcon"] + cooldown = ( + skill_data.get("cdTime", 0) if "cooldown" in skill_cls.__fields__ else None + ) + attributes = get_skill_attributes(skill_data["proudSkillGroupId"]) + return skill_cls( + **{ + i[0]: i[1] + for i in zip( + ["name", "description", "icon", "cooldown", "attributes"], + [_name, _description, icon, cooldown, attributes], + ) + if i is not None + } + ) + + +def parse_passive_talent( + talent_data: dict, talent_cls: type[PassiveTalent] +) -> PassiveTalent: + group_id = talent_data["proudSkillGroupId"] + _promote_level = talent_data.get("needAvatarPromoteLevel", 0) + skill_data = next( + filter(lambda x: x["proudSkillGroupId"] == group_id, proud_skill_json_data) + ) + param_descriptions = list( + filter( + lambda x: x is not None, + map( + lambda x: manager.get_text(x), + skill_data["paramDescList"], + ), + ) + ) + _description = manager.get_text(skill_data["descTextMapHash"]) + _param_list = skill_data["paramList"][ + : len(re.findall(r"(\d+(?:\.)?\d+)", remove_rich_tag(_description) or "")) + ] + return talent_cls( + name=manager.get_text(skill_data["nameTextMapHash"]) or "", + description=_description or "", + icon=skill_data["icon"], + promote_level=_promote_level, + attribute=TalentAttribute( + param_descriptions=param_descriptions, + param_list=_param_list, + break_level=skill_data.get("breakLevel", 0), + ), + ) + + +# noinspection PyShadowingBuiltins,SpellCheckingInspection,PyGlobalUndefined async def parse_avatar_data(lang: Lang): + global out_path, manager + global avatar_json_data, fetter_info_json_data, story_json_data, promote_json_data + global skill_depot_json_data, skill_json_data, proud_skill_json_data, talent_json_data out_path = OUT_DIR.joinpath(f"{lang}") out_path.mkdir(exist_ok=True, parents=True) @@ -64,6 +234,7 @@ async def parse_avatar_data(lang: Lang): skill_depot_json_data = manager.fetch("AvatarSkillDepotExcelConfigData") skill_json_data = manager.fetch("AvatarSkillExcelConfigData") proud_skill_json_data = manager.fetch("ProudSkillExcelConfigData") + talent_json_data = manager.fetch("AvatarTalentExcelConfigData") avatar_list = [] for data in avatar_json_data: @@ -71,25 +242,36 @@ async def parse_avatar_data(lang: Lang): if ( info_data := next( chain( - filter(lambda x: x["avatarId"] == id, fetter_info_json_data), - [ - None, - ], + filter(lambda x: x["avatarId"] == id, fetter_info_json_data), [None] ) ) ) is None: continue name = manager.get_text(data["nameTextMapHash"]) - element = elements[info_data["avatarVisionBeforTextMapHash"]] - quality = AvatarQuality(data["qualityType"].removeprefix("QUALITY_")) + element = next( + filter( + lambda x: info_data["avatarVisionBeforTextMapHash"] in x[0], + elements_map.items(), + ) + )[1] + quality = AvatarQuality( + data["qualityType"].removeprefix("QUALITY_").replace("ORANGE_SP", "SPECIAL") + ) weapon_type = next( - filter(lambda x: x in data["weaponType"], WeaponType.__members__.values()) + filter( + lambda x: x in data["weaponType"].replace("POLE", "POLEARM"), + WeaponType.__members__.values(), + ) ) # 角色信息 title = manager.get_text(info_data["avatarTitleTextMapHash"]) - birth = AvatarBirth( - month=info_data["infoBirthMonth"], day=info_data["infoBirthDay"] + birth = ( + AvatarBirth( + month=info_data["infoBirthMonth"], day=info_data["infoBirthDay"] + ) + if id not in [10000005, 10000007] + else None ) occupation = manager.get_text(info_data["avatarNativeTextMapHash"]) vision = manager.get_text(info_data["avatarVisionBeforTextMapHash"]) @@ -106,36 +288,30 @@ async def parse_avatar_data(lang: Lang): en=manager.get_text(info_data["cvEnglishTextMapHash"]), kr=manager.get_text(info_data["cvKoreanTextMapHash"]), ) - story_data = sorted( + story_datas = sorted( filter(lambda x: x["avatarId"] == id, story_json_data), key=lambda x: x["fetterId"], ) - stories = AvatarStories( - **{ - i[0]: i[1] - for i in zip( - AvatarStories.__fields__.keys(), - map( - lambda x: Story( - title=manager.get_text(x["storyTitleTextMapHash"]), - content=manager.get_text(x["storyContextTextMapHash"]), - tips=list( - filter( - lambda y: y is not None, - map( - lambda z: manager.get_text(z), - x["tips"], - ), - ) - ), - ), - sorted( - filter(lambda x: x["avatarId"] == id, story_data), - key=lambda x: x["fetterId"], - ), - ), + stories = [] + for story_data in sorted( + filter(lambda x: x["avatarId"] == id, story_datas), + key=lambda x: x["fetterId"], + ): + tips = list( + filter( + lambda x: x is not None, + map(lambda x: manager.get_text(x), story_data["tips"]), ) - } + ) + story = Story( + title=manager.get_text(story_data["storyTitleTextMapHash"]), + content=manager.get_text(story_data["storyContextTextMapHash"]), + tips=tips, + ) + stories.append(story) + + avatar_stories = AvatarStories( + **{i[0]: i[1] for i in zip(AvatarStories.__fields__, stories)} ) information = AvatarInfo( title=title, @@ -146,11 +322,11 @@ async def parse_avatar_data(lang: Lang): description=description, association=association, seuyu=seuyu, - stories=stories, + stories=avatar_stories, ) # 角色基础属性 - attribute = AvatarAttribute( + attributes = AvatarAttribute( HP=data["hpBase"], Attack=data["attackBase"], Defense=data["defenseBase"], @@ -159,117 +335,6 @@ async def parse_avatar_data(lang: Lang): ChargeEfficiency=data["chargeEfficiency"], ) - def get_skill_attributes(proud_skill_group_id: int) -> list[TalentAttribute]: - proud_skill_datas = sorted( - filter( - lambda x: x["proudSkillGroupId"] == proud_skill_group_id, - proud_skill_json_data, - ), - key=lambda x: x["level"], - ) - result: list[TalentAttribute] = [] - for proud_skill_data in proud_skill_datas: - param_descriptions = list( - filter( - lambda x: x is not None, - map( - lambda x: manager.get_text(x), - proud_skill_data["paramDescList"], - ), - ) - ) - param_num = len(param_descriptions) - for param_description in param_descriptions: - param_num = max( - param_num, - *map(int, re.findall(r"param(\d*)\:", param_description)), - ) - result.append( - TalentAttribute( - level=proud_skill_data["level"], - param_descriptions=param_descriptions, - param_list=proud_skill_data["paramList"][:param_num], - break_level=proud_skill_data.get("breakLevel", 0), - coin=proud_skill_data.get("coinCost", 0), - cost_items=list( - map( - lambda x: ItemCount( - item_id=x["id"], - count=x["count"], - ), - filter(lambda x: x, proud_skill_data["costItems"]), - ) - ), - ) - ) - return result - - def parse_skill(skill_id: int, skill_cls: type[CombatTalent]) -> CombatTalent: - skill_data = next(filter(lambda x: x["id"] == skill_id, skill_json_data)) - _name = manager.get_text(skill_data["nameTextMapHash"]) - _description = manager.get_text(skill_data["descTextMapHash"]) - icon = skill_data["skillIcon"] - cooldown = ( - skill_data.get("cdTime", 0) - if "cooldown" in skill_cls.__fields__.keys() - else None - ) - attributes = get_skill_attributes(skill_data["proudSkillGroupId"]) - return skill_cls( - **{ - i[0]: i[1] - for i in zip( - ["name", "description", "icon", "cooldown", "attributes"], - [_name, _description, icon, cooldown, attributes], - ) - if i is not None - } - ) - - def parse_passive_talent( - talent_data: dict, talent_cls: type[PassiveTalent] - ) -> PassiveTalent: - group_id = talent_data["proudSkillGroupId"] - _promote_level = talent_data.get("needAvatarPromoteLevel", 0) - skill_data = next( - filter( - lambda x: x["proudSkillGroupId"] == group_id, proud_skill_json_data - ) - ) - param_descriptions = list( - filter( - lambda x: x is not None, - map( - lambda x: manager.get_text(x), - skill_data["paramDescList"], - ), - ) - ) - _description = manager.get_text(skill_data["descTextMapHash"]) - _param_list = skill_data["paramList"][ - : len( - re.findall( - r"(\d+)|(?:(\d+)%)|(?:(\d+\.\d+))", - re.sub( - r"(<(?P[a-z]+?)=(?P.+?)>.+)", - "", - _description, - ), - ) - ) - ] - return talent_cls( - name=manager.get_text(skill_data["nameTextMapHash"]), - description=_description, - icon=skill_data["icon"], - promote_level=_promote_level, - attribute=TalentAttribute( - param_descriptions=param_descriptions, - param_list=_param_list, - break_level=skill_data.get("breakLevel", 0), - ), - ) - # 天赋 skill_depot_data = next( filter(lambda x: x["id"] == data["skillDepotId"], skill_depot_json_data) @@ -278,36 +343,47 @@ async def parse_avatar_data(lang: Lang): # 普通攻击 normal_attack = parse_skill(skill_ids[0], NormalAttack) # 元素战技 - elemental_skill = parse_skill(skill_ids[1], ElementalSkill) + elemental_skill = ( + parse_skill(skill_ids[1], ElementalSkill) + if id not in [10000005, 10000007] + else None + ) # 冲刺技能 alternate_sprint = ( parse_skill(skill_ids[2], AlternateSprint) if len(skill_ids) == 3 else None ) # 元素爆发 - burst_skill = parse_skill(skill_depot_data["energySkill"], ElementalBurst) + burst_skill = ( + parse_skill(skill_depot_data["energySkill"], ElementalBurst) + if id not in [10000005, 10000007] + else None + ) # 第一次突破被动天赋 - first_passive = parse_passive_talent( - skill_depot_data["inherentProudSkillOpens"][0], FirstAscensionPassive + first_passive = ( + parse_passive_talent( + skill_depot_data["inherentProudSkillOpens"][0], FirstAscensionPassive + ) + if id not in [10000005, 10000007] + else None ) # 第四次突破被动天赋 - fourth_passive = parse_passive_talent( - skill_depot_data["inherentProudSkillOpens"][1], FourthAscensionPassive + fourth_passive = ( + parse_passive_talent( + skill_depot_data["inherentProudSkillOpens"][1], FourthAscensionPassive + ) + if id not in [10000005, 10000007] + else None ) # 实用固有天赋 - utility_passive = parse_passive_talent( - skill_depot_data["inherentProudSkillOpens"][2], UtilityPassive + utility_passive = ( + parse_passive_talent( + skill_depot_data["inherentProudSkillOpens"][2], UtilityPassive + ) + if id not in [10000005, 10000007] + else None ) # 杂项固有天赋 - if ( - len( - list( - filter( - lambda x: x != 0, skill_depot_data["inherentProudSkillOpens"] - ) - ) - ) - == 4 - ): + if skill_depot_data["inherentProudSkillOpens"][3]: miscellaneous_passive = parse_passive_talent( skill_depot_data["inherentProudSkillOpens"][3], MiscellaneousPassive ) @@ -335,7 +411,7 @@ async def parse_avatar_data(lang: Lang): for promote_data in promote_datas: items = [] for item_data in promote_data["costItems"]: - if item_data: + if item_data and id not in [10000005, 10000007]: items.append( ItemCount(item_id=item_data["id"], count=item_data["count"]) ) @@ -347,4 +423,47 @@ async def parse_avatar_data(lang: Lang): cost_items=items, ) ) - breakpoint() + + # 角色命座信息 + constellations: list[AvatarConstellation] = [] + for constellation_id in filter(lambda x: x != 0, skill_depot_data["talents"]): + constellation_data = next( + filter(lambda x: x["talentId"] == constellation_id, talent_json_data) + ) + constellation_description = manager.get_text( + constellation_data["descTextMapHash"] + ) + # noinspection PyTypeChecker + constellations.append( + AvatarConstellation( + name=manager.get_text(constellation_data["nameTextMapHash"]), + description=constellation_description, + icon=constellation_data["icon"], + param_list=list( + filter(lambda x: x != 0.0, constellation_data["paramList"]) + ), + ) + ) + avatar = Avatar( + id=id, + name=name, + element=element, + quality=quality, + weapon_type=weapon_type, + information=information, + attributes=attributes, + talents=avatar_talents, + promotes=promotes, + constellations=constellations, + ) + avatar_list.append(avatar) + async with async_open(out_path / "avatar.json", encoding="utf-8", mode="w") as file: + await file.write( + json.dumps( + [i.dict() for i in avatar_list], + ensure_ascii=False, + encode_html_chars=False, + indent=4, + ), + ) + return out_path, avatar_list diff --git a/utils/funcs.py b/utils/funcs.py new file mode 100644 index 0000000..b5a40bf --- /dev/null +++ b/utils/funcs.py @@ -0,0 +1,14 @@ +try: + import regex as re +except ImportError: + import re + +__all__ = ("remove_rich_tag",) + + +def remove_rich_tag(string: str | None) -> str: + """去除富文本标签""" + if string is not None: + return re.sub( + r"(<(?P[a-z]+?)=(?P.+?)>.+?)", "", string + )