from __future__ import annotations
import re
from typing import List, Dict, Literal, cast, TYPE_CHECKING
from .utils import Lookup
from .mixins import CacheClient, CacheObject
from .enums import DeviceType, AbilityType, Rarity
if TYPE_CHECKING:
from . import responses
from .items import Device
from .enums import Language
from .cache import DataCache
__all__ = [
"Skin",
"Ability",
"Champion",
]
def _card_ability_sort(card: Device) -> str:
ability = card.ability
if type(ability) == CacheObject:
return f"z{ability.name}" # push the card to the very end
return ability.name
[docs]class Ability(CacheObject):
"""
Represents a Champion's Ability.
You can find these on the `Champion.abilities` attribute.
Inherits from `CacheObject`.
Attributes
----------
name : str
The name of the ability.
id : int
The ID of the ability.
champion : Champion
The champion this ability belongs to.
description : str
The description of the ability.
type : AbilityType
The type of the ability (currently only damage type).
cooldown : int
The ability's cooldown, in seconds.
icon_url : str
A URL of this ability's icon.
"""
_desc_pattern = re.compile(r" ?<br>(?:<br>)? ?") # replace the <br> tags with a new line
def __init__(self, champion: Champion, ability_data: responses.AbilityObject):
super().__init__(id=ability_data["Id"], name=ability_data["Summary"])
self.champion = champion
desc = ability_data["Description"].strip().replace('\r', '')
self.description: str = self._desc_pattern.sub('\n', desc)
self.type = AbilityType(ability_data["damageType"], _return_default=True)
self.cooldown: int = ability_data["rechargeSeconds"]
self.icon_url: str = ability_data["URL"]
__hash__ = CacheObject.__hash__
[docs]class Skin(CacheObject):
"""
Represents a Champion's Skin and it's information.
You can get these from the `Champion.get_skins` method,
as well as find on various other objects returned from the API.
Inherits from `CacheObject`.
Attributes
----------
name : str
The name of the skin.
id : int
The ID of the skin.
champion : Champion
The champion this skin belongs to.
rarity : Rarity
The skin's rarity.
"""
def __init__(self, champion: Champion, skin_data: responses.ChampionSkinObject):
# pre-process champion and skin name
self.champion: Champion = champion
skin_name = skin_data["skin_name"]
if skin_name.endswith(self.champion.name):
skin_name = skin_name[:-len(self.champion.name)].strip()
super().__init__(id=skin_data["skin_id2"], name=skin_name)
rarity: str = skin_data["rarity"]
self.rarity: Rarity
if rarity: # not an empty string
self.rarity = Rarity(rarity, _return_default=True)
else:
self.rarity = Rarity.Default
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}: {self._name} {self.champion.name}"
f"({self.rarity.name}, {self._id})"
)
[docs]class Champion(CacheObject, CacheClient):
"""
Represents a Champion and it's information.
You can find these on the `CacheEntry.champions` attribute,
as well as various other objects returned from the API.
Inherits from `CacheObject`.
.. note::
An object of this class can be `False` in a boolean context, if it's internal state
is deemed incomplete or corrupted. For the internal state to be considered valid, there has
to be exactly 16 cards and 3 talents assigned to the champion. If you don't plan on
accessing / processing those, you can use the ``is not None`` in the check instead.
Examples:
.. code-block:: py
if champion:
# champion exists and is valid
if not champion:
# champion doesn't exist, or exists in an invalid state
if champion is not None:
# champion exists but might be invalid
if champion is None:
# champion doesn't exist
Attributes
----------
name : str
The name of the champion.
id : int
The ID of the champion.
title : str
The champion's title.
role : Literal["Front Line", "Support", "Damage", "Flank"]
The champion's role.
lore : str
The champion's lore.
icon_url : str
A URL of this champion's icon.
health : int
The amount of health points this champion has at base.
speed : int
The champion's speed.
abilities : Lookup[Ability]
An object that lets you iterate over all abilities this champion has.\n
Use ``list(...)`` to get a list instead.
.. note::
Some champions may have more than 5 abilities - this will happen if one of their
abilities allows switching other abilities between their states.
talents : Lookup[Device]
An object that lets you iterate over all talents this champion has.\n
Use ``list(...)`` to get a list instead.
cards : Lookup[Device]
An iterator that lets you iterate over all cards this champion has.\n
Use ``list(...)`` to get a list instead.
skins : Lookup[Skin]
An object that lets you iterate over all skins this champion has.\n
Use ``list(...)`` to get a list instead.
"""
_name_pattern = re.compile(r'([a-z ]+)(?:/\w+)? \(([a-z ]+)\)', re.I)
_desc_pattern = re.compile(r'([A-Z][a-zA-Z ]+): ([\w\s\-\'%,.]+)(?:<br><br>|[\r\n]?\n|$)')
_url_pattern = re.compile(r'([a-z\-]+)(?=\.(?:jpg|png))')
def __init__(
self,
cache: DataCache,
language: Language,
champion_data: responses.ChampionObject,
devices: List[Device],
skins_data: List[responses.ChampionSkinObject],
):
CacheClient.__init__(self, cache)
CacheObject.__init__(self, id=champion_data["id"], name=champion_data["Name"])
self._language = language
self.title: str = champion_data["Title"]
self.role = cast(
Literal["Front Line", "Support", "Damage", "Flank"],
champion_data["Roles"][9:].replace("er", ""),
)
self.icon_url: str = champion_data["ChampionIcon_URL"]
self.lore: str = champion_data["Lore"]
self.health: int = champion_data["Health"]
self.speed: int = champion_data["Speed"]
# Abilities
abilities = []
for i in range(1, 6):
ability_data = champion_data[f"Ability_{i}"] # type: ignore[misc]
# see if this is a composite ability
match = self._name_pattern.match(ability_data["Summary"])
if match:
# yes - we need to split the data into two sets
composites: Dict[str, responses.AbilityObject] = {}
name1, name2 = match.groups()
composites[name1] = {"Summary": name1} # type: ignore[typeddict-item]
composites[name2] = {"Summary": name2} # type: ignore[typeddict-item]
descs = self._desc_pattern.findall(ability_data["Description"])
for ability_name, ability_desc in descs:
ability_dict = composites.get(ability_name)
if ability_dict is None:
continue
ability_dict["Description"] = ability_desc
# modify the URL
ability_dict["URL"] = self._url_pattern.sub(
ability_name.lower().replace(' ', '-'), ability_data["URL"]
)
# copy the rest of attributes
ability_dict["Id"] = ability_data["Id"]
ability_dict["damageType"] = ability_data["damageType"]
ability_dict["rechargeSeconds"] = ability_data["rechargeSeconds"]
# add the ability
abilities.append(Ability(self, ability_dict))
else:
# nope - just append it
abilities.append(Ability(self, ability_data))
self.abilities: Lookup[Ability, Ability] = Lookup(abilities)
# Talents and Cards
cards: List[Device] = []
talents: List[Device] = []
for d in devices:
if d.type == DeviceType.Card:
cards.append(d)
elif d.type == DeviceType.Talent: # pragma: no branch
talents.append(d)
d._attach_champion(self) # requires the abilities to exist already
talents.sort(key=lambda d: d.unlocked_at)
cards.sort(key=lambda d: d.name)
cards.sort(key=_card_ability_sort)
self.cards: Lookup[Device, Device] = Lookup(cards)
self.talents: Lookup[Device, Device] = Lookup(talents)
# Skins
self.skins: Lookup[Skin, Skin] = Lookup(
sorted((Skin(self, d) for d in skins_data), key=lambda s: s.rarity.value)
)
__hash__ = CacheObject.__hash__
def __bool__(self) -> bool:
return len(self.cards) == 16 and len(self.talents) == 3
[docs] async def get_skins(self) -> List[Skin]:
"""
Returns a list of skins this champion has.
.. note::
This information is cached under the `skins` attribute.
Returns
-------
List[Skin]
The list of skins available for this champion.
"""
response = await self._api.request("getchampionskins", self.id, self._language.value)
self.skins = Lookup(
sorted((Skin(self, skin_data) for skin_data in response), key=lambda s: s.rarity.value)
)
return list(self.skins)