mirror of
https://github.com/PaiGramTeam/SIMNet.git
synced 2024-11-21 21:58:05 +00:00
✨ Add Calculator Client
This commit is contained in:
parent
fcc1f2489b
commit
c2f562884e
0
simnet/client/components/calculator/__init__.py
Normal file
0
simnet/client/components/calculator/__init__.py
Normal file
370
simnet/client/components/calculator/genshin.py
Normal file
370
simnet/client/components/calculator/genshin.py
Normal file
@ -0,0 +1,370 @@
|
||||
from typing import Optional, Any, Dict, List
|
||||
|
||||
from simnet.client.base import BaseClient
|
||||
from simnet.client.routes import CALCULATOR_URL
|
||||
from simnet.errors import BadRequest
|
||||
from simnet.models.genshin.calculator import (
|
||||
CalculatorResult,
|
||||
CalculatorCharacter,
|
||||
CalculatorWeapon,
|
||||
CalculatorArtifact,
|
||||
CalculatorFurnishing,
|
||||
CalculatorCharacterDetails,
|
||||
CalculatorTalent,
|
||||
)
|
||||
from simnet.utils.enum_ import Region
|
||||
from simnet.utils.player import recognize_genshin_server
|
||||
|
||||
|
||||
class CalculatorClient(BaseClient):
|
||||
"""A client for retrieving data from Genshin's calculator component."""
|
||||
|
||||
async def request_calculator(
|
||||
self,
|
||||
endpoint: str,
|
||||
*,
|
||||
method: str = "POST",
|
||||
lang: Optional[str] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Make a request towards the calculator endpoint.
|
||||
|
||||
Args:
|
||||
endpoint (str): The calculator endpoint to send the request to.
|
||||
method (str): The HTTP method to use for the request (default "POST").
|
||||
lang (str): The language to use for the request (default None).
|
||||
params (dict): The parameters to include in the request URL (default None).
|
||||
data (dict): The data to include in the request body (default None).
|
||||
|
||||
Returns:
|
||||
dict: The data returned by the calculator endpoint.
|
||||
"""
|
||||
params = dict(params or {})
|
||||
|
||||
base_url = CALCULATOR_URL.get_url(self.region)
|
||||
url = base_url / endpoint
|
||||
|
||||
if method == "GET":
|
||||
params["lang"] = lang or self.lang
|
||||
data = None
|
||||
else:
|
||||
data = dict(data or {})
|
||||
data["lang"] = lang or self.lang
|
||||
|
||||
headers = {}
|
||||
if self.region == Region.CHINESE:
|
||||
headers["Referer"] = (
|
||||
"https://webstatic.mihoyo.com/ys/event/e20200923adopt_calculator/index.html?"
|
||||
"bbs_presentation_style=fullscreen&bbs_auth_required=true&"
|
||||
"utm_source=bbs&utm_medium=mys&utm_campaign=icon#/"
|
||||
)
|
||||
|
||||
data = await self.request_lab(
|
||||
url, method=method, params=params, data=data, headers=headers
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
async def _execute_calculator(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> CalculatorResult:
|
||||
"""Calculate the results of a builder.
|
||||
|
||||
Args:
|
||||
data (dict): The data used to calculate the results.
|
||||
lang (str): The language to use for the request (default None).
|
||||
|
||||
Returns:
|
||||
CalculatorResult: The calculated results.
|
||||
"""
|
||||
data = await self.request_calculator("compute", lang=lang, data=data)
|
||||
return CalculatorResult(**data)
|
||||
|
||||
async def _enable_calculator_sync(self, enabled: bool = True) -> None:
|
||||
"""Enable data syncing in calculator.
|
||||
|
||||
Args:
|
||||
enabled (bool): Whether to enable syncing (default True).
|
||||
"""
|
||||
await self.request_calculator(
|
||||
"avatar/auth", method="POST", data=dict(avatar_auth=int(enabled))
|
||||
)
|
||||
|
||||
async def _get_calculator_items(
|
||||
self,
|
||||
slug: str,
|
||||
filters: Dict[str, Any],
|
||||
query: Optional[str] = None,
|
||||
*,
|
||||
player_id: Optional[int] = None,
|
||||
is_all: bool = False,
|
||||
sync: bool = False,
|
||||
lang: Optional[str] = None,
|
||||
autoauth: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get all items of a specific slug from a calculator.
|
||||
|
||||
Args:
|
||||
slug (str): The slug to get the items for.
|
||||
filters (dict): The filters to apply to the items.
|
||||
query (str): The query to search for (default None).
|
||||
player_id (int): The player ID to use for syncing (default None).
|
||||
is_all (bool): Whether to include Traveler items (default False).
|
||||
sync (bool): Whether to sync data from the calculator (default False).
|
||||
lang (str): The language to use for the request (default None).
|
||||
autoauth (bool): Whether to enable syncing if it is not already enabled (default True).
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries representing the items retrieved from the calculator.
|
||||
"""
|
||||
endpoint = f"sync/{slug}/list" if sync else f"{slug}/list"
|
||||
|
||||
if query:
|
||||
if any(filters.values()):
|
||||
raise TypeError("Cannot specify a query and filter at the same time")
|
||||
|
||||
filters = dict(keywords=query, **filters)
|
||||
|
||||
payload: Dict[str, Any] = dict(page=1, size=69420, is_all=is_all, **filters)
|
||||
|
||||
if sync:
|
||||
player_id = player_id or self.player_id
|
||||
payload["uid"] = player_id
|
||||
payload["region"] = recognize_genshin_server(player_id)
|
||||
|
||||
try:
|
||||
data = await self.request_calculator(endpoint, lang=lang, data=payload)
|
||||
except BadRequest as e:
|
||||
if e.ret_code != -502002: # Sync not enabled
|
||||
raise
|
||||
if not autoauth:
|
||||
raise BadRequest(e.response, "Calculator sync is not enabled") from e
|
||||
|
||||
await self._enable_calculator_sync()
|
||||
data = await self.request_calculator(endpoint, lang=lang, data=payload)
|
||||
|
||||
return data["list"]
|
||||
|
||||
async def get_calculator_characters(
|
||||
self,
|
||||
*,
|
||||
query: Optional[str] = None,
|
||||
elements: Optional[List[int]] = None,
|
||||
weapon_types: Optional[List[int]] = None,
|
||||
include_traveler: bool = False,
|
||||
sync: bool = False,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorCharacter]:
|
||||
"""Get all characters provided by the Enhancement Progression Calculator.
|
||||
|
||||
Args:
|
||||
query (str): The query to search for (default None).
|
||||
elements (list): A list of element IDs to filter by (default None).
|
||||
weapon_types (list): A list of weapon type IDs to filter by (default None).
|
||||
include_traveler (bool): Whether to include Traveler characters (default False).
|
||||
sync (bool): Whether to sync data from the calculator (default False).
|
||||
player_id (int): The player ID to use for syncing (default None).
|
||||
lang (str): The language to use for the request (default None).
|
||||
|
||||
Returns:
|
||||
list: A list of CalculatorCharacter objects representing the characters retrieved from the calculator.
|
||||
"""
|
||||
data = await self._get_calculator_items(
|
||||
"avatar",
|
||||
lang=lang,
|
||||
is_all=include_traveler,
|
||||
sync=sync,
|
||||
player_id=player_id,
|
||||
query=query,
|
||||
filters=dict(
|
||||
element_attr_ids=elements or [],
|
||||
weapon_cat_ids=weapon_types or [],
|
||||
),
|
||||
)
|
||||
return [CalculatorCharacter(**i) for i in data]
|
||||
|
||||
async def get_calculator_weapons(
|
||||
self,
|
||||
*,
|
||||
query: Optional[str] = None,
|
||||
types: Optional[List[int]] = None,
|
||||
rarities: Optional[List[int]] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorWeapon]:
|
||||
"""Get all weapons provided by the Enhancement Progression Calculator.
|
||||
|
||||
Args:
|
||||
query (Optional[str], optional): A query string to filter the results. Defaults to None.
|
||||
types (Optional[List[int]], optional): A list of weapon types to include in the results. Defaults to None.
|
||||
rarities (Optional[List[int]], optional): A list of weapon rarities to include in the results.
|
||||
Defaults to None.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[CalculatorWeapon]: A list of weapons provided by the Enhancement Progression Calculator.
|
||||
"""
|
||||
data = await self._get_calculator_items(
|
||||
"weapon",
|
||||
lang=lang,
|
||||
query=query,
|
||||
filters=dict(
|
||||
weapon_cat_ids=types or [],
|
||||
weapon_levels=rarities or [],
|
||||
),
|
||||
)
|
||||
return [CalculatorWeapon(**i) for i in data]
|
||||
|
||||
async def get_calculator_furnishings(
|
||||
self,
|
||||
*,
|
||||
types: Optional[int] = None,
|
||||
rarities: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorFurnishing]:
|
||||
"""
|
||||
Get all furnishings provided by the Enhancement Progression Calculator.
|
||||
|
||||
Args:
|
||||
types (Optional[int], optional): The type of furnishings to retrieve. Defaults to None.
|
||||
rarities (Optional[int], optional): The rarity of the furnishings to retrieve. Defaults to None.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[CalculatorFurnishing]: A list of furnishings provided by the Enhancement Progression Calculator.
|
||||
"""
|
||||
data = await self._get_calculator_items(
|
||||
"furniture",
|
||||
lang=lang,
|
||||
filters=dict(
|
||||
cat_id=types or 0,
|
||||
weapon_levels=rarities or 0,
|
||||
),
|
||||
)
|
||||
return [CalculatorFurnishing(**i) for i in data]
|
||||
|
||||
async def get_character_details(
|
||||
self,
|
||||
character: int,
|
||||
*,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> CalculatorCharacterDetails:
|
||||
"""
|
||||
Get the weapon, artifacts and talents of a character.
|
||||
|
||||
Args:
|
||||
character (int): The ID of the character to retrieve details for.
|
||||
player_id (Optional[int], optional): The player ID to use for the request. Defaults to None.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
CalculatorCharacterDetails: The details of the character.
|
||||
"""
|
||||
player_id = player_id or self.player_id
|
||||
|
||||
data = await self.request_calculator(
|
||||
"sync/avatar/detail",
|
||||
method="GET",
|
||||
lang=lang,
|
||||
params=dict(
|
||||
avatar_id=int(character),
|
||||
uid=player_id,
|
||||
region=recognize_genshin_server(player_id),
|
||||
),
|
||||
)
|
||||
return CalculatorCharacterDetails(**data)
|
||||
|
||||
async def get_character_talents(
|
||||
self,
|
||||
character: int,
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorTalent]:
|
||||
"""Get the talents of a character.
|
||||
|
||||
Args:
|
||||
character (int): The ID of the character to retrieve talents for.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[CalculatorTalent]: A list of talents for the specified character.
|
||||
"""
|
||||
data = await self.request_calculator(
|
||||
"avatar/skill_list",
|
||||
method="GET",
|
||||
lang=lang,
|
||||
params=dict(avatar_id=int(character)),
|
||||
)
|
||||
return [CalculatorTalent(**i) for i in data["list"]]
|
||||
|
||||
async def get_complete_artifact_set(
|
||||
self,
|
||||
artifact: int,
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorArtifact]:
|
||||
"""
|
||||
Get all artifacts that share a set with a specified artifact.
|
||||
|
||||
Args:
|
||||
artifact (int): The ID of the artifact to retrieve the set for.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[CalculatorArtifact]: A list of artifacts that share a set with the specified artifact.
|
||||
"""
|
||||
data = await self.request_calculator(
|
||||
"reliquary/set",
|
||||
method="GET",
|
||||
lang=lang,
|
||||
params=dict(reliquary_id=int(artifact)),
|
||||
)
|
||||
return [CalculatorArtifact(**i) for i in data["reliquary_list"]]
|
||||
|
||||
async def _get_all_artifact_ids(self, artifact_id: int) -> List[int]:
|
||||
"""Get all artifact IDs in the same set as a given artifact ID.
|
||||
|
||||
Args:
|
||||
artifact_id (int): The ID of the artifact to retrieve the set for.
|
||||
|
||||
Returns:
|
||||
List[int]: A list of artifact IDs that share a set with the specified artifact ID.
|
||||
"""
|
||||
others = await self.get_complete_artifact_set(artifact_id)
|
||||
return [artifact_id] + [other.id for other in others]
|
||||
|
||||
async def get_teapot_replica_blueprint(
|
||||
self,
|
||||
share_code: int,
|
||||
*,
|
||||
region: Optional[str] = None,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> List[CalculatorFurnishing]:
|
||||
"""Get the furnishings used by a teapot replica blueprint.
|
||||
|
||||
Args:
|
||||
share_code (int): The share code of the teapot replica blueprint to retrieve furnishings for.
|
||||
region (Optional[str], optional): The region to use for the request. Defaults to None.
|
||||
player_id (Optional[int], optional): The player ID to use for the request. Defaults to None.
|
||||
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[CalculatorFurnishing]: A list of furnishings used by the specified teapot replica blueprint.
|
||||
"""
|
||||
if not region:
|
||||
player_id = player_id or self.player_id
|
||||
region = recognize_genshin_server(player_id)
|
||||
|
||||
data = await self.request_calculator(
|
||||
"furniture/blueprint",
|
||||
method="GET",
|
||||
lang=lang,
|
||||
params=dict(share_code=share_code, region=region),
|
||||
)
|
||||
return [CalculatorFurnishing(**i) for i in data["list"]]
|
390
simnet/models/genshin/calculator.py
Normal file
390
simnet/models/genshin/calculator.py
Normal file
@ -0,0 +1,390 @@
|
||||
"""Genshin calculator models."""
|
||||
|
||||
|
||||
import collections
|
||||
from typing import Dict, Any, Literal, Optional, List
|
||||
|
||||
from pydantic import Field, validator
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
from simnet.models.genshin.character import BaseCharacter
|
||||
|
||||
__all__ = (
|
||||
"CALCULATOR_ARTIFACTS",
|
||||
"CALCULATOR_ELEMENTS",
|
||||
"CALCULATOR_WEAPON_TYPES",
|
||||
"CalculatorArtifact",
|
||||
"CalculatorArtifactResult",
|
||||
"CalculatorCharacter",
|
||||
"CalculatorCharacterDetails",
|
||||
"CalculatorConsumable",
|
||||
"CalculatorFurnishing",
|
||||
"CalculatorFurnishingResults",
|
||||
"CalculatorResult",
|
||||
"CalculatorTalent",
|
||||
"CalculatorWeapon",
|
||||
)
|
||||
CALCULATOR_ELEMENTS: Dict[int, str] = {
|
||||
1: "Pyro",
|
||||
2: "Anemo",
|
||||
3: "Geo",
|
||||
4: "Dendro",
|
||||
5: "Electro",
|
||||
6: "Hydro",
|
||||
7: "Cryo",
|
||||
}
|
||||
CALCULATOR_WEAPON_TYPES: Dict[int, str] = {
|
||||
1: "Sword",
|
||||
10: "Catalyst",
|
||||
11: "Claymore",
|
||||
12: "Bow",
|
||||
13: "Polearm",
|
||||
}
|
||||
CALCULATOR_ARTIFACTS: Dict[int, str] = {
|
||||
1: "Flower of Life",
|
||||
2: "Plume of Death",
|
||||
3: "Sands of Eon",
|
||||
4: "Goblet of Eonothem",
|
||||
5: "Circlet of Logos",
|
||||
}
|
||||
|
||||
|
||||
class CalculatorCharacter(BaseCharacter):
|
||||
"""Character meant to be used with calculators.
|
||||
|
||||
Attributes:
|
||||
rarity (int): The rarity of the character.
|
||||
element (str): The element of the character.
|
||||
weapon_type (str): The type of weapon used by the character.
|
||||
level (int): The current level of the character.
|
||||
max_level (int): The maximum level of the character.
|
||||
"""
|
||||
|
||||
rarity: int = Field(alias="avatar_level")
|
||||
element: str = Field(alias="element_attr_id")
|
||||
weapon_type: str = Field(alias="weapon_cat_id")
|
||||
level: int = Field(alias="level_current", default=0)
|
||||
max_level: int
|
||||
|
||||
@validator("element", pre=True)
|
||||
def parse_element(cls, v: Any) -> str:
|
||||
"""Parse the element of a character.
|
||||
|
||||
Args:
|
||||
v (Any): The value of the element.
|
||||
|
||||
Returns:
|
||||
str: The parsed element.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
|
||||
return CALCULATOR_ELEMENTS[int(v)]
|
||||
|
||||
@validator("weapon_type", pre=True)
|
||||
def parse_weapon_type(cls, v: Any) -> str:
|
||||
"""Parse the weapon type of a character.
|
||||
|
||||
Args:
|
||||
v (Any): The value of the weapon type.
|
||||
|
||||
Returns:
|
||||
str: The parsed weapon type.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
|
||||
return CALCULATOR_WEAPON_TYPES[int(v)]
|
||||
|
||||
|
||||
class CalculatorWeapon(APIModel):
|
||||
"""Weapon meant to be used with calculators.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the weapon.
|
||||
name (str): The name of the weapon.
|
||||
icon (str): The icon of the weapon.
|
||||
rarity (int): The rarity of the weapon.
|
||||
type (str): The type of weapon.
|
||||
level (int): The current level of the weapon.
|
||||
max_level (int): The maximum level of the weapon.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
icon: str
|
||||
rarity: int = Field(alias="weapon_level")
|
||||
type: str = Field(alias="weapon_cat_id")
|
||||
level: int = Field(alias="level_current", default=0)
|
||||
max_level: int
|
||||
|
||||
@validator("type", pre=True)
|
||||
def parse_weapon_type(cls, v: Any) -> str:
|
||||
"""
|
||||
Parse the type of a weapon.
|
||||
|
||||
Args:
|
||||
v (Any): The value of the weapon type.
|
||||
|
||||
Returns:
|
||||
str: The parsed weapon type.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
|
||||
return CALCULATOR_WEAPON_TYPES[int(v)]
|
||||
|
||||
|
||||
class CalculatorArtifact(APIModel):
|
||||
"""Artifact meant to be used with calculators.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the artifact.
|
||||
name (str): The name of the artifact.
|
||||
icon (str): The icon of the artifact.
|
||||
rarity (int): The rarity of the artifact.
|
||||
pos (int): The position of the artifact.
|
||||
level (int): The current level of the artifact.
|
||||
max_level (int): The maximum level of the artifact.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
icon: str
|
||||
rarity: int = Field(alias="reliquary_level")
|
||||
pos: int = Field(alias="reliquary_cat_id")
|
||||
level: int = Field(alias="level_current", default=0)
|
||||
max_level: int
|
||||
|
||||
@property
|
||||
def pos_name(self) -> str:
|
||||
"""The name of the artifact position.
|
||||
|
||||
Returns:
|
||||
str: The name of the artifact position.
|
||||
"""
|
||||
return CALCULATOR_ARTIFACTS[self.pos]
|
||||
|
||||
|
||||
class CalculatorTalent(APIModel):
|
||||
"""Talent of a character meant to be used with calculators.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the talent.
|
||||
group_id (int): The group ID of the talent.
|
||||
name (str): The name of the talent.
|
||||
icon (str): The icon of the talent.
|
||||
level (int): The current level of the talent.
|
||||
max_level (int): The maximum level of the talent.
|
||||
"""
|
||||
|
||||
id: int
|
||||
group_id: int # proudSkillGroupId
|
||||
name: str
|
||||
icon: str
|
||||
level: int = Field(alias="level_current", default=0)
|
||||
max_level: int
|
||||
|
||||
@property
|
||||
def type(self) -> Literal["attack", "skill", "burst", "passive", "dash"] | None:
|
||||
"""The type of the talent, parsed from the group ID.
|
||||
|
||||
Returns:
|
||||
Literal["attack", "skill", "burst", "passive", "dash"] | None: The type of the talent.
|
||||
Returns `None` if the type cannot be determined.
|
||||
"""
|
||||
# special cases
|
||||
if self.id == self.group_id:
|
||||
return "passive" # maybe hoyo does this for upgradable?
|
||||
|
||||
if len(str(self.id)) == 6: # in candSkillDepotIds
|
||||
return "attack"
|
||||
|
||||
# 4139 -> group=41 identifier=3 order=9
|
||||
_, relevant = divmod(self.group_id, 100)
|
||||
identifier, order = divmod(relevant, 10)
|
||||
|
||||
if identifier == 2:
|
||||
return "passive"
|
||||
if order == 1:
|
||||
return "attack"
|
||||
if order == 2:
|
||||
return "skill"
|
||||
if order == 9:
|
||||
return "burst"
|
||||
if order == 3:
|
||||
return "dash"
|
||||
return None
|
||||
|
||||
@property
|
||||
def upgradeable(self) -> bool:
|
||||
"""Whether this talent can be leveled up.
|
||||
|
||||
Returns:
|
||||
bool: Whether this talent can be leveled up.
|
||||
"""
|
||||
return self.type not in ("passive", "dash")
|
||||
|
||||
|
||||
class CalculatorFurnishing(APIModel):
|
||||
"""Furnishing meant to be used with calculators.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the furnishing.
|
||||
name (str): The name of the furnishing.
|
||||
icon (str): The icon URL of the furnishing.
|
||||
rarity (int): The rarity level of the furnishing.
|
||||
amount (int, optional): The amount of the furnishing, if applicable.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
icon: str = Field(alias="icon_url")
|
||||
rarity: int = Field(alias="level")
|
||||
|
||||
amount: Optional[int] = Field(alias="num")
|
||||
|
||||
|
||||
class CalculatorCharacterDetails(APIModel):
|
||||
"""Details of a synced calculator
|
||||
|
||||
Attributes:
|
||||
weapon (CalculatorWeapon): The calculator weapon.
|
||||
talents (List[CalculatorTalent]): A list of calculator talents.
|
||||
artifacts (List[CalculatorArtifact]): A list of calculator artifacts.
|
||||
"""
|
||||
|
||||
weapon: CalculatorWeapon = Field(alias="weapon")
|
||||
talents: List[CalculatorTalent] = Field(alias="skill_list")
|
||||
artifacts: List[CalculatorArtifact] = Field(alias="reliquary_list")
|
||||
|
||||
@validator("talents")
|
||||
def correct_talent_current_level(
|
||||
cls, v: List[CalculatorTalent]
|
||||
) -> List[CalculatorTalent]:
|
||||
"""Validates the current level of each calculator talent in the talents list and sets it to 1 if it is 0.
|
||||
|
||||
Args:
|
||||
cls: The class.
|
||||
v (List[CalculatorTalent]): The list of calculator talents to validate.
|
||||
|
||||
Returns:
|
||||
List[CalculatorTalent]: The list of validated calculator talents.
|
||||
"""
|
||||
# passive talent have current levels at 0 for some reason
|
||||
talents: List[CalculatorTalent] = []
|
||||
|
||||
for talent in v:
|
||||
if talent.max_level == 1 and talent.level == 0:
|
||||
raw = talent.dict()
|
||||
raw["level"] = 1
|
||||
talent = CalculatorTalent(**raw)
|
||||
|
||||
talents.append(talent)
|
||||
|
||||
return v
|
||||
|
||||
@property
|
||||
def upgradeable_talents(self) -> List[CalculatorTalent]:
|
||||
"""Returns a list of all calculator talents that can be leveled up.
|
||||
|
||||
Returns:
|
||||
List[CalculatorTalent]: A list of all calculator talents that can be leveled up.
|
||||
"""
|
||||
if self.talents[2].type == "dash":
|
||||
return [self.talents[0], self.talents[1], self.talents[3]]
|
||||
return [self.talents[0], self.talents[1], self.talents[2]]
|
||||
|
||||
|
||||
class CalculatorConsumable(APIModel):
|
||||
"""Item consumed when upgrading.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the consumable.
|
||||
name (str): The name of the consumable.
|
||||
icon (str): The URL of the icon of the consumable.
|
||||
amount (int): The number of this consumable used.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
icon: str
|
||||
amount: int = Field(alias="num")
|
||||
|
||||
|
||||
class CalculatorArtifactResult(APIModel):
|
||||
"""Calculation result for a specific artifact.
|
||||
|
||||
Attributes:
|
||||
artifact_id (int): The ID of the artifact.
|
||||
list (List[CalculatorConsumable]): A list of CalculatorConsumable objects representing the consumables
|
||||
used by this artifact.
|
||||
"""
|
||||
|
||||
artifact_id: int = Field(alias="reliquary_id")
|
||||
list: List[CalculatorConsumable] = Field(alias="id_consume_list")
|
||||
|
||||
|
||||
class CalculatorResult(APIModel):
|
||||
"""
|
||||
Calculation result.
|
||||
|
||||
Attributes:
|
||||
character (List[CalculatorConsumable]): Consumables used by characters.
|
||||
weapon (List[CalculatorConsumable]): Consumables used by weapons.
|
||||
talents (List[CalculatorConsumable]): Consumables used by talents.
|
||||
artifacts (List[CalculatorArtifactResult]): Artifacts used.
|
||||
"""
|
||||
|
||||
character: List[CalculatorConsumable] = Field(alias="avatar_consume")
|
||||
weapon: List[CalculatorConsumable] = Field(alias="weapon_consume")
|
||||
talents: List[CalculatorConsumable] = Field(alias="avatar_skill_consume")
|
||||
artifacts: List[CalculatorArtifactResult] = Field(alias="reliquary_consume")
|
||||
|
||||
@property
|
||||
def total(self) -> List[CalculatorConsumable]:
|
||||
"""Returns the total consumables used across all categories.
|
||||
|
||||
Returns:
|
||||
List[CalculatorConsumable]: A list of CalculatorConsumable objects representing the total
|
||||
consumables used across all categories.
|
||||
"""
|
||||
artifacts = [i for a in self.artifacts for i in a.list]
|
||||
combined = self.character + self.weapon + self.talents + artifacts
|
||||
|
||||
grouped: Dict[int, List[CalculatorConsumable]] = collections.defaultdict(list)
|
||||
for i in combined:
|
||||
grouped[i.id].append(i)
|
||||
|
||||
total = [
|
||||
CalculatorConsumable(
|
||||
id=x[0].id,
|
||||
name=x[0].name,
|
||||
icon=x[0].icon,
|
||||
num=sum(i.amount for i in x),
|
||||
)
|
||||
for x in grouped.values()
|
||||
]
|
||||
|
||||
return total
|
||||
|
||||
|
||||
class CalculatorFurnishingResults(APIModel):
|
||||
"""Furnishing calculation result.
|
||||
|
||||
Attributes:
|
||||
furnishings (List[CalculatorConsumable]): A list of CalculatorConsumable objects representing the
|
||||
furnishings used.
|
||||
"""
|
||||
|
||||
furnishings: List[CalculatorConsumable] = Field(alias="list")
|
||||
|
||||
@property
|
||||
def total(self) -> List[CalculatorConsumable]:
|
||||
"""Returns the total furnishings used.
|
||||
|
||||
Returns:
|
||||
List[CalculatorConsumable]: A list of CalculatorConsumable objects representing the total
|
||||
furnishings used.
|
||||
"""
|
||||
return self.furnishings
|
35
tests/test_genshin_calculator_client.py
Normal file
35
tests/test_genshin_calculator_client.py
Normal file
@ -0,0 +1,35 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from simnet.client.components.calculator.genshin import CalculatorClient
|
||||
from simnet.utils.enum_ import Region
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from simnet.client.cookies import Cookies
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def calculator_client(
|
||||
genshin_player_id: int, account_id: int, cookies: "Cookies"
|
||||
):
|
||||
async with CalculatorClient(
|
||||
player_id=genshin_player_id,
|
||||
cookies=cookies,
|
||||
account_id=account_id,
|
||||
region=Region.CHINESE,
|
||||
) as client_instance:
|
||||
yield client_instance
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCalculatorClient:
|
||||
@staticmethod
|
||||
async def test_character_details(calculator_client: "CalculatorClient"):
|
||||
character_details = await calculator_client.get_character_details(10000046)
|
||||
assert len(character_details.talents) == 6
|
||||
for talent in character_details.talents:
|
||||
assert talent.level == 10 or talent.max_level == 13 or talent.max_level == 1
|
||||
for artifact in character_details.artifacts:
|
||||
assert artifact.level >= 0
|
Loading…
Reference in New Issue
Block a user