SIMNet/simnet/models/genshin/calculator.py

389 lines
11 KiB
Python

"""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 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 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) -> Optional[Literal["attack", "skill", "burst", "passive", "dash"]]:
"""The type of the talent, parsed from the group ID.
Returns:
Optional[Literal["attack", "skill", "burst", "passive", "dash"]]: 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, optional): The calculator weapon.
talents (List[CalculatorTalent]): A list of calculator talents.
artifacts (List[CalculatorArtifact]): A list of calculator artifacts.
"""
weapon: Optional[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.
consumable_list (List[CalculatorConsumable]): A list of CalculatorConsumable objects representing the
consumables used by this artifact.
"""
artifact_id: int = Field(alias="reliquary_id")
consumable_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.consumable_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