diff --git a/simnet/client/components/chronicle/genshin.py b/simnet/client/components/chronicle/genshin.py index f8ae09a..e74437e 100644 --- a/simnet/client/components/chronicle/genshin.py +++ b/simnet/client/components/chronicle/genshin.py @@ -5,6 +5,7 @@ from simnet.client.components.chronicle.base import BaseChronicleClient from simnet.client.routes import RECORD_URL from simnet.errors import DataNotPublic, BadRequest from simnet.models.genshin.chronicle.abyss import SpiralAbyss, SpiralAbyssPair +from simnet.models.genshin.chronicle.character_detail import GenshinCharacterListInfo, GenshinDetailCharacters from simnet.models.genshin.chronicle.characters import Character from simnet.models.genshin.chronicle.img_theater import ImgTheater from simnet.models.genshin.chronicle.notes import Notes, NotesWidget, NotesOverseaWidget @@ -323,3 +324,47 @@ class GenshinBattleChronicleClient(BaseChronicleClient): data = await self._request_genshin_record("widget/v2", endpoint_type="aapi", lang=lang) model = NotesWidget return model(**data) + + async def get_genshin_character_list( + self, + player_id: Optional[int] = None, + *, + lang: Optional[str] = None, + ) -> List[GenshinCharacterListInfo]: + """Retrieve a list of Genshin Impact character information for a player. + + Args: + player_id (Optional[int]): The ID of the player. Defaults to None. + lang (Optional[str]): The language for the character information. Defaults to None. + + Returns: + List[GenshinCharacterListInfo]: A list of GenshinCharacterListInfo objects containing character details. + """ + + data = await self._request_genshin_record("character/list", player_id, method="POST", lang=lang) + return [GenshinCharacterListInfo(**i) for i in data["list"]] + + async def get_genshin_character_detail( + self, + characters: List[int], + player_id: Optional[int] = None, + *, + lang: Optional[str] = None, + ) -> GenshinDetailCharacters: + """Retrieve detailed information about Genshin Impact characters. + + Args: + characters (List[int]): The IDs of the characters to retrieve details for. + player_id (Optional[int]): The ID of the player. Defaults to None. + lang (Optional[str]): The language for the character information. Defaults to None. + + Returns: + GenshinDetailCharacters: An object containing detailed information. + """ + + ids = [characters] if isinstance(characters, int) else characters + payload = {"character_ids": ids} + data = await self._request_genshin_record( + "character/detail", player_id, method="POST", lang=lang, payload=payload + ) + return GenshinDetailCharacters(**data) diff --git a/simnet/models/genshin/chronicle/character_detail.py b/simnet/models/genshin/chronicle/character_detail.py new file mode 100644 index 0000000..9abda3c --- /dev/null +++ b/simnet/models/genshin/chronicle/character_detail.py @@ -0,0 +1,143 @@ +import enum +import typing + +import pydantic +from pydantic import Field + +from simnet.models.base import APIModel +from simnet.models.genshin.chronicle.characters import ( + PartialCharacter, + CharacterWeapon, + Outfit, + Constellation, + Artifact, +) + + +class GenshinWeaponType(enum.IntEnum): + """Character weapon types.""" + + SWORD = 1 + CATALYST = 10 + CLAYMORE = 11 + BOW = 12 + POLEARM = 13 + + +class GenshinCharacterListInfoWeapon(APIModel): + """A class representing information about a Genshin Impact character's weapon.""" + + id: int + icon: str + type: GenshinWeaponType + rarity: int + level: int + affix_level: int + + +class GenshinCharacterListInfo(PartialCharacter): + """A class representing detailed information about a Genshin Impact character.""" + + icon: str + image: str + side_icon: str + + weapon_type: GenshinWeaponType + weapon: GenshinCharacterListInfoWeapon + + +class PropInfo(APIModel): + """A property such as Crit Rate, HP, HP%.""" + + type: int = Field(alias="property_type") + name: str + icon: typing.Optional[str] + filter_name: str + + @pydantic.validator("name", "filter_name") + @classmethod + def __fix_names(cls, value: str) -> str: # skipcq: PTC-W0038 + r"""Fix "\xa0" in Crit Damage + Crit Rate names.""" + return value.replace("\xa0", " ") + + +class PropertyValue(APIModel): + """A property with a value.""" + + property_type: int + base: str + add: str + final: str + + +class DetailCharacterWeapon(CharacterWeapon): + """Detailed Genshin Weapon with main/sub stats.""" + + type: GenshinWeaponType + type_name: str + + main_property: PropertyValue + sub_property: typing.Optional[PropertyValue] = None + + +class ArtifactProperty(APIModel): + """Artifact's Property value & roll count.""" + + property_type: int + value: str + times: int + + +class DetailArtifact(Artifact): + """Detailed artifact with main/sub stats.""" + + main_property: ArtifactProperty + sub_property_list: typing.Sequence[ArtifactProperty] + + +class SkillAffix(APIModel): + """Skill affix texts.""" + + name: str + value: str + + +class CharacterSkill(APIModel): + """Character's skill.""" + + id: int = Field(alias="skill_id") + skill_type: int + name: str + level: int + + description: str = Field(alias="desc") + affixes: typing.Sequence[SkillAffix] = Field(alias="skill_affix_list") + icon: str + is_unlocked: bool = Field(alias="is_unlock") + + +class GenshinDetailCharacter(APIModel): + """Full Detailed Genshin Character""" + + base: GenshinCharacterListInfo + weapon: DetailCharacterWeapon + artifacts: typing.Sequence[DetailArtifact] = Field(alias="relics") + + constellations: typing.Sequence[Constellation] + costumes: typing.Sequence[Outfit] + + skills: typing.Sequence[CharacterSkill] + + selected_properties: typing.Sequence[PropertyValue] + base_properties: typing.Sequence[PropertyValue] + extra_properties: typing.Sequence[PropertyValue] + element_properties: typing.Sequence[PropertyValue] + + +class GenshinDetailCharacters(APIModel): + """Genshin character list.""" + + characters: typing.Sequence[GenshinDetailCharacter] = Field(alias="list") + + property_map: typing.Mapping[str, PropInfo] + relic_property_options: typing.Mapping[str, typing.Sequence[int]] diff --git a/tests/test_genshin_battle_chronicle_client.py b/tests/test_genshin_battle_chronicle_client.py index fdb0943..6dcab31 100644 --- a/tests/test_genshin_battle_chronicle_client.py +++ b/tests/test_genshin_battle_chronicle_client.py @@ -54,3 +54,8 @@ class TestGenshinBattleChronicleClient: async def test_get_genshin_imaginarium_theater(genshin_client: GenshinBattleChronicleClient): data = await genshin_client.get_genshin_imaginarium_theater() assert len(data.data) > 0 + + @staticmethod + async def test_get_genshin_character_list(genshin_client: GenshinBattleChronicleClient): + data = await genshin_client.get_genshin_character_list() + assert len(data) > 0