SIMNet/simnet/models/genshin/wish.py

314 lines
9.4 KiB
Python

import re
from datetime import datetime
from enum import IntEnum
from typing import Any, Optional, List
from pydantic import Field, validator
from simnet.models.base import APIModel
class BannerType(IntEnum):
"""
Enumeration of banner types in wish histories.
Attributes:
NOVICE (100): Temporary novice banner.
STANDARD (200): Permanent standard banner.
CHARACTER (301): Rotating character banner.
WEAPON (302): Rotating weapon banner.
CHARACTER1 (301): Special case, first character banner.
CHARACTER2 (400): Special case, second character banner.
CHRONICLED (500): Chronicled wish.
"""
NOVICE = 100
"""Temporary novice banner."""
STANDARD = PERMANENT = 200
"""Permanent standard banner."""
CHARACTER = 301
"""Rotating character banner."""
WEAPON = 302
"""Rotating weapon banner."""
# these are special cases
# they exist inside the history but should be counted as the same
CHARACTER1 = 301
"""Character banner #1."""
CHARACTER2 = 400
"""Character banner #2."""
CHRONICLED = 500
"""Chronicled wish."""
class Wish(APIModel):
"""
Model for a wish made on any banner.
Attributes:
uid (int): User ID.
id (int): Wish ID.
type (str): Item type.
name (str): Item name.
rarity (int): Item rarity.
time (datetime): Wish timestamp.
banner_type (BannerType): Type of banner.
banner_name (str): Name of banner.
"""
uid: int
id: int
type: str = Field(alias="item_type")
name: str
rarity: int = Field(alias="rank_type")
time: datetime
banner_type: BannerType = Field(alias="gacha_type")
banner_name: str
@validator("banner_type", pre=True)
def cast_banner_type(cls, v: Any) -> int:
"""Converts the banner type to an integer.
Args:
v (Any): The value to be validated.
Returns:
int: The validated integer value.
"""
return int(v)
class BannerDetailItem(APIModel):
"""Represents an item that may be obtained from a banner.
Attributes:
name (str): The name of the item.
type (str): The type of the item.
rarity (int): The rarity of the item.
up (bool): Whether the item has a rate-up on the current banner.
order (int): The order value of the item.
"""
name: str = Field(alias="item_name")
type: str = Field(alias="item_type")
rarity: int = Field(alias="rank")
up: bool = Field(alias="is_up")
order: int = Field(alias="order_value")
class BannerDetailsUpItem(APIModel):
"""Represents an item that has a rate-up on a banner.
Attributes:
name (str): The name of the item.
type (str): The type of the item.
element (str): The element of the item. Valid values are: "", "", "", "", "", "", "", "".
icon (str): The URL of the item's icon.
"""
name: str = Field(alias="item_name")
type: str = Field(alias="item_type")
element: str = Field(alias="item_attr")
icon: str = Field(alias="item_img")
@validator("element", pre=True)
def parse_element(cls, v: str) -> str:
"""Converts the element string to a standardized format.
Args:
v (str): The value to be validated.
Returns:
str: The standardized element string.
"""
return {
"": "Anemo",
"": "Pyro",
"": "Hydro",
"": "Electro",
"": "Cryo",
"": "Geo",
"": "Dendro",
"": "",
}.get(v, v)
class BannerDetails(APIModel):
"""Details of a gacha banner.
Attributes:
banner_id (str): The unique ID of the banner.
banner_type (int): The type of the banner.
100: Novice Wishes
200: Permanent Wish
301: Character Event Wish
302: Weapon Event Wish
400: Character Event Wish
title (str): The title of the banner.
content (str): The description of the banner.
date_range (str): The duration of the banner
in the format of "YYYY-MM-DD HH:MM - YYYY-MM-DD HH:MM" in UTC timezone.
r5_up_prob (float, optional): The probability of getting a 5-star item that is currently featured in the banner.
r4_up_prob (float, optional): The probability of getting a 4-star item that is currently featured in the banner.
r5_prob (float, optional): The probability of getting any 5-star item in the banner.
r4_prob (float, optional): The probability of getting any 4-star item in the banner.
r3_prob (float, optional): The probability of getting any 3-star item in the banner.
r5_guarantee_prob (float, optional): The probability of getting a 5-star item after a certain number of pulls
(pity system) in the banner.
r4_guarantee_prob (float, optional): The probability of getting a 4-star item after a certain number of pulls
(pity system) in the banner.
r3_guarantee_prob (float, optional): The probability of getting a 3-star item after a certain number of pulls
(pity system) in the banner.
r5_up_items (List[BannerDetailsUpItem]): A list of featured 5-star items in the banner.
r4_up_items (List[BannerDetailsUpItem]): A list of featured 4-star items in the banner.
r5_items (List[BannerDetailItem]): A list of all 5-star items in the banner.
r4_items (List[BannerDetailItem]): A list of all 4-star items in the banner.
r3_items (List[BannerDetailItem]): A list of all 3-star items in the banner.
"""
banner_id: str
banner_type: int = Field(alias="gacha_type")
title: str
content: str
date_range: str
r5_up_prob: Optional[float]
r4_up_prob: Optional[float]
r5_prob: Optional[float]
r4_prob: Optional[float]
r3_prob: Optional[float]
r5_guarantee_prob: Optional[float] = Field(alias="r5_baodi_prob")
r4_guarantee_prob: Optional[float] = Field(alias="r4_baodi_prob")
r3_guarantee_prob: Optional[float] = Field(alias="r3_baodi_prob")
r5_up_items: List[BannerDetailsUpItem]
r4_up_items: List[BannerDetailsUpItem]
r5_items: List[BannerDetailItem] = Field(alias="r5_prob_list")
r4_items: List[BannerDetailItem] = Field(alias="r4_prob_list")
r3_items: List[BannerDetailItem] = Field(alias="r3_prob_list")
@validator("r5_up_items", "r4_up_items", pre=True)
def replace_none(cls, v: Optional[List[Any]]) -> List[Any]:
"""Replaces NoneType attributes with an empty list.
Args:
v (Optional[List[Any]]): The input list.
Returns:
List[Any]: The input list with NoneType attributes replaced with an empty list.
"""
return v or []
@validator(
"r5_up_prob",
"r4_up_prob",
"r5_prob",
"r4_prob",
"r3_prob",
"r5_guarantee_prob",
"r4_guarantee_prob",
"r3_guarantee_prob",
pre=True,
)
def parse_percentage(cls, v: Optional[str]) -> Optional[float]:
"""Parses percentage strings into float values.
Args:
v (Optional[str]): The input string.
Returns:
Optional[float]: The float value of the input string, or None if the input is None or already a float.
"""
if v is None or isinstance(v, (int, float)):
return v
return None if v == "0%" else float(v[:-1].replace(",", "."))
@property
def name(self) -> str:
"""Returns the name of the banner without HTML tags.
Returns:
str: The name of the banner.
"""
return re.sub(r"<.*?>", "", self.title).strip()
@property
def banner_type_name(self) -> str:
"""Returns the name of the banner type based on the `banner_type` attribute.
Returns:
str: The name of the banner type.
"""
banners = {
100: "Novice Wishes",
200: "Permanent Wish",
301: "Character Event Wish",
302: "Weapon Event Wish",
400: "Character Event Wish",
}
return banners[self.banner_type]
@property
def items(self) -> List[BannerDetailItem]:
"""Returns a list of all items in the banner sorted by their order.
Returns:
List[BannerDetailItem]: A list of all items in the banner sorted by their order.
"""
items = self.r5_items + self.r4_items + self.r3_items
return sorted(items, key=lambda x: x.order)
class GachaItem(APIModel):
"""An item that can be obtained from a gacha pull.
Attributes:
name (str): The name of the item.
type (str): The type of the item.
rarity (int): The rarity of the item.
id (int): The ID of the item.
"""
name: str
type: str = Field(alias="item_type")
rarity: int = Field(alias="rank_type")
id: int = Field(alias="item_id")
@validator("id")
def format_id(cls, v: int) -> int:
"""Formats the `id` attribute to a standardized 8-digit format.
Args:
v (int): The input ID.
Returns:
int: The formatted ID.
"""
return 10000000 + v - 1000 if len(str(v)) == 4 else v
def is_character(self) -> bool:
"""Returns whether the item is a character.
Returns:
bool: True if the item is a character, False otherwise.
"""
return len(str(self.id)) == 8