from typing import Dict, List, Set, Union
from PIL.Image import Image as PILImage
import arrow
from merakicommons.cache import lazy, lazy_property
from merakicommons.container import searchable, SearchableList, SearchableDictionary
from ... import configuration
from ...data import Resource, Region, Platform, GameMode, Key
from ..common import (
CoreData,
CassiopeiaObject,
CassiopeiaGhost,
CassiopeiaLazyList,
CoreDataList,
get_latest_version,
ghost_load_on,
)
from .common import ImageData, Image, Sprite
from .map import Map
from ...dto.staticdata import champion as dto
from .item import Item
##############
# Data Types #
##############
class ChampionReleaseData(CoreData):
_renamed = {}
class ChampionRatesData(CoreData):
_dto_type = dto.ChampionRatesDto
_renamed = {}
class ChampionListData(CoreDataList):
_dto_type = dto.ChampionListDto
_renamed = {"included_data": "includedData"}
class SpellVarsData(CoreData):
_renamed = {"dyn": "dynamic", "coeff": "coefficients"}
class LevelTipData(CoreData):
_renamed = {"effect": "effects", "label": "keywords"}
class ChampionSpellData(CoreData):
_renamed = {
"vars": "variables",
"maxrank": "maxRank",
"cooldown": "cooldowns",
"cost": "costs",
"effect": "effects",
"costType": "cost_type",
"keyboardKey": "keyboard_key",
}
def __call__(self, *args, **kwargs):
if "leveltip" in kwargs:
self.levelUpTips = LevelTipData(**kwargs.pop("leveltip"))
if "vars" in kwargs:
self.variables = [SpellVarsData(**v) for v in kwargs.pop("vars")]
if "image" in kwargs:
self.image = ImageData(version=kwargs["version"], **kwargs.pop("image"))
if "altimages" in kwargs:
self.alternative_images = [
ImageData(version=kwargs["version"], **alt)
for alt in kwargs.pop("altimages")
]
super().__call__(**kwargs)
return self
class BlockItemData(CoreData):
_renamed = {}
class BlockData(CoreData):
_renamed = {}
def __call__(self, *args, **kwargs):
if "items" in kwargs:
self.items = [BlockItemData(**item) for item in kwargs.pop("items")]
super().__call__(**kwargs)
return self
class RecommendedData(CoreData):
_renamed = {}
def __call__(self, *args, **kwargs):
if "blocks" in kwargs:
self.itemSets = [BlockData(**item) for item in kwargs.pop("blocks")]
super().__call__(**kwargs)
return self
class PassiveData(CoreData):
_renamed = {}
def __call__(self, **kwargs):
if "image" in kwargs:
self.image = ImageData(version=kwargs["version"], **kwargs.pop("image"))
super().__call__(**kwargs)
return self
class SkinData(CoreData):
_renamed = {"num": "number"}
class StatsData(CoreData):
_renamed = {
"armorperlevel": "armorPerLevel",
"hpperlevel": "healthPerLevel",
"attackdamage": "attackDamage",
"mpperlevel": "manaPerLevel",
"attackspeed": "attackSpeedOffset",
"hp": "health",
"hpregenperlevel": "healthRegenPerLevel",
"attackspeedperlevel": "percentAttackSpeedPerLevel",
"attackrange": "attackRange",
"attackdamageperlevel": "attackDamagePerLevel",
"mpregenperlevel": "manaRegenPerLevel",
"mp": "mana",
"spellblockperlevel": "magicResistPerLevel",
"crit": "criticalStrikeChance",
"mpregen": "manaRegen",
"spellblock": "magicResist",
"hpregen": "healthRegen",
"critperlevel": "criticalStrikeChancePerLevel",
}
class InfoData(CoreData):
_renamed = {}
class ChampionData(CoreData):
_dto_type = dto.ChampionDto
_renamed = {
"allytips": "allyTips",
"enemytips": "enemyTips",
"recommended": "recommendedItemsets",
"partype": "resource",
"included_data": "includedData",
}
def __call__(self, **kwargs):
if "recommended" in kwargs:
self.recommendedItemsets = [
RecommendedData(**item) for item in kwargs.pop("recommended")
]
if "info" in kwargs:
self.info = InfoData(**kwargs.pop("info"))
if "stats" in kwargs:
self.stats = StatsData(**kwargs.pop("stats"))
if "image" in kwargs:
self.image = ImageData(**kwargs.pop("image"))
if "skins" in kwargs:
self.skins = [SkinData(**skin) for skin in kwargs.pop("skins")]
if "passive" in kwargs:
version = kwargs.get(
"version", get_latest_version(kwargs["region"], endpoint="champion")
)
self.passive = PassiveData(version=version, **kwargs.pop("passive"))
if "spells" in kwargs:
try:
version
except NameError:
version = kwargs.get(
"version", get_latest_version(kwargs["region"], endpoint="champion")
)
self.spells = [
ChampionSpellData(version=version, **spell)
for spell in kwargs.pop("spells")
]
super().__call__(**kwargs)
return self
##############
# Core Types #
##############
[docs]class Champions(CassiopeiaLazyList):
_data_types = {ChampionListData}
def __init__(
self,
*,
region: Union[Region, str] = None,
version: str = None,
locale: str = None,
included_data: Set[str] = None
):
if included_data is None:
included_data = {"all"}
if locale is None and region is not None:
locale = Region(region).default_locale
kwargs = {"region": region, "included_data": included_data, "locale": locale}
if version:
kwargs["version"] = version
CassiopeiaObject.__init__(self, **kwargs)
@lazy_property
def region(self) -> Region:
"""The region for this champion."""
return Region(self._data[ChampionListData].region)
@lazy_property
def platform(self) -> Platform:
"""The platform for this champion."""
return self.region.platform
@property
def version(self) -> str:
"""The version for this champion."""
try:
return self._data[ChampionListData].version
except AttributeError:
version = get_latest_version(region=self.region, endpoint="champion")
self(version=version)
return self._data[ChampionListData].version
@property
def locale(self) -> str:
"""The locale for this champion."""
return self._data[ChampionListData].locale
@property
def included_data(self) -> Set[str]:
"""A set of tags to return additonal information for this champion when it's loaded."""
return self._data[ChampionListData].includedData
[docs]@searchable({str: ["key"]})
class SpellVars(CassiopeiaObject):
_data_types = {SpellVarsData}
@property
def ranks_with(self) -> str:
"""Well, we don't know what this one is. let us know if you figure it out."""
return self._data[SpellVarsData].ranksWith
@property
def dynamic(self) -> str:
"""Well, we don't know what this one is. let us know if you figure it out."""
return self._data[SpellVarsData].dynamic
@property
def link(self) -> str:
"""Stat this spell scales from."""
return self._data[SpellVarsData].link
@lazy_property
def coefficients(self) -> List[float]:
"""The scaling coefficients for this spell."""
return SearchableList(self._data[SpellVarsData].coefficients)
@property
def key(self) -> str:
"""Well, we don't know what this one is. let us know if you figure it out."""
return self._data[SpellVarsData].key
[docs]@searchable({str: ["name", "key", "keywords"], Key: ["keyboard_key"]})
class ChampionSpell(CassiopeiaObject):
_data_types = {ChampionSpellData}
@lazy_property
def keywords(self) -> List[str]:
"""The keywords for this spell."""
return SearchableList(self._data[ChampionSpellData].levelUpTips.keywords)
@property
def effects_by_level(self) -> List[str]:
"""The level-up changes, level-by-level."""
return SearchableList(self._data[ChampionSpellData].levelUpTips.effects)
@lazy_property
def variables(self) -> List[SpellVars]:
"""Contains spell data."""
return SearchableList(
SpellVars.from_data(v) for v in self._data[ChampionSpellData].variables
)
@property
def resource(self) -> Resource:
"""The resource consumed when using this spell."""
return Resource(self._data[ChampionSpellData].cost_type)
@lazy_property
def image_info(self) -> Image:
"""The info about the spell's image, which can be pulled from datadragon."""
return Image.from_data(self._data[ChampionSpellData].image)
@property
def sanitized_description(self) -> str:
"""The spell's sanitized description."""
return self._data[ChampionSpellData].sanitizedDescription
@property
def sanitized_tooltip(self) -> str:
"""The spell's sanitized tooltip."""
return self._data[ChampionSpellData].sanitizedTooltip
@property
def effects(self) -> List[List[float]]:
"""The level-by-level replacements for {{ e# }} tags in other values."""
return SearchableList(self._data[ChampionSpellData].effects)
@property
def tooltip(self) -> str:
"""The spell's tooltip."""
return self._data[ChampionSpellData].tooltip
@property
def max_rank(self) -> int:
"""The maximum rank this spell can attain."""
return self._data[ChampionSpellData].max_rank
@property
def range(self) -> List[Union[int, str]]:
"""The maximum range of this spell. `self` if it has no range."""
return SearchableList(self._data[ChampionSpellData].range)
@property
def cooldowns(self) -> List[float]:
"""The cooldowns of this spell (per level)."""
return SearchableList(self._data[ChampionSpellData].cooldowns)
@property
def costs(self) -> List[int]:
"""The resource costs of this spell (per level)."""
return SearchableList(self._data[ChampionSpellData].costs)
@property
def key(self) -> str:
"""The spell's key."""
return self._data[ChampionSpellData].key
@property
def description(self) -> str:
"""The spell's description."""
return self._data[ChampionSpellData].description
@lazy_property
def alternative_images(self) -> List[Image]:
"""The alternative images for this spell. These won't exist after patch NN, when Riot standardized all images."""
return SearchableList(
Image.from_data(alt)
for alt in self._data[ChampionSpellData].alternativeImages
)
@property
def name(self) -> str:
"""The spell's name."""
return self._data[ChampionSpellData].name
@property
def keyboard_key(self) -> Key:
"""Q, W, E, or R"""
return Key(self._data[ChampionSpellData].keyboard_key)
[docs]@searchable({str: ["type", "items"], Item: ["items"]})
class ItemSet(CassiopeiaObject):
_data_types = {BlockData}
def __init__(self, **kwargs):
if "region" in kwargs:
self.__region = kwargs["region"]
super().__init__(**kwargs)
[docs] @classmethod
def from_data(cls, data: CoreData, region: Region):
self = super().from_data(data)
self.__region = region
return self
@property
def items(self) -> Dict[Item, int]:
"""A dictionary of items mapped to how many of them are recommended."""
return SearchableDictionary(
{
Item(id=item.id, region=self.__region): item.count
for item in self._data[BlockData].items
}
) # TODO Add version; low priority and hopefully it's just never an issue
@property
def rec_math(self) -> bool:
"""Well, we don't know what this one is. let us know if you figure it out."""
return self._data[BlockData].recMath
@property
def type(self) -> str:
"""The item set's type (e.g. starting items)."""
return self._data[BlockData].type
[docs]@searchable(
{
str: ["item_sets", "map", "title", "mode", "type"],
Item: ["item_sets"],
GameMode: ["mode"],
}
)
class RecommendedItems(CassiopeiaObject):
_data_types = {RecommendedData}
def __init__(self, **kwargs):
if "region" in kwargs:
self.__region = kwargs["region"]
super().__init__(**kwargs)
[docs] @classmethod
def from_data(cls, data: CoreData, region: Region):
self = super().from_data(data)
self.__region = region
return self
@lazy_property
def map(self) -> Map:
"""The name of the map these recommendations are for."""
convert_shitty_map_name_to_id = {"HA": 12, "CS": 8, "SR": 11, "TT": 10}
id_ = convert_shitty_map_name_to_id[self._data[RecommendedData].map]
return Map(
id=id_, region=self.__region
) # TODO Add version; low priority and hopefully it's just never an issue
@lazy_property
def item_sets(self) -> List[ItemSet]:
"""The recommended item sets."""
return SearchableList(
ItemSet.from_data(itemset, region=self.__region)
for itemset in self._data[RecommendedData].itemSets
)
@property
def title(self) -> str:
"""The title of these recommendations."""
return self._data[RecommendedData].title
@property
def priority(self) -> bool:
"""Whether this is a priority recommendation."""
return self._data[RecommendedData].priority
@property
def mode(self) -> GameMode:
"""The game mode these recommendations are for."""
return GameMode(self._data[RecommendedData].mode)
@property
def type(self) -> str:
"""The type of recommendation."""
return self._data[RecommendedData].type
[docs]@searchable({str: ["name"]})
class Passive(CassiopeiaObject):
_data_types = {PassiveData}
@lazy_property
def image_info(self) -> Image:
"""The info about the spell's image, which can be pulled from datadragon."""
return Image.from_data(self._data[PassiveData].image)
@property
def sanitized_description(self) -> str:
"""The spell's sanitized description."""
return self._data[PassiveData].sanitizedDescription
@property
def name(self) -> str:
"""The spell's name."""
return self._data[PassiveData].name
@property
def description(self) -> str:
"""The spells' description."""
return self._data[PassiveData].description
[docs]@searchable({str: ["name", "splash_url", "loading_image_url"], int: ["id"]})
class Skin(CassiopeiaObject):
_data_types = {SkinData}
def __init__(self, **kwargs):
if "champion_key" in kwargs:
self.__champion_key = kwargs.pop("champion_key")
super().__init__(**kwargs)
@property
def champion_key(self) -> str:
"""The key for the champion this belongs to."""
return self.__champion_key
@property
def number(self) -> int:
"""The skin number."""
return self._data[SkinData].number
@property
def name(self) -> str:
"""The skin's name."""
return self._data[SkinData].name
@property
def id(self) -> int:
"""The skin's ID."""
return self._data[SkinData].id
@property
def splash_url(self) -> str:
"""The skin's splash art url."""
return "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/{}_{}.jpg".format(
self.champion_key, self.number
)
@property
def loading_image_url(self) -> str:
"""The skin's loading screen image url."""
return "https://ddragon.leagueoflegends.com/cdn/img/champion/loading/{}_{}.jpg".format(
self.champion_key, self.number
)
@lazy_property
def splash(self) -> PILImage:
"""The skin's splash art."""
return configuration.settings.pipeline.get(
PILImage, query={"url": self.splash_url}
)
@lazy_property
def loading_image(self) -> PILImage:
"""The skin's loading screen image."""
return configuration.settings.pipeline.get(
PILImage, query={"url": self.loading_image_url}
)
[docs]class Stats(CassiopeiaObject):
_data_types = {StatsData}
@property
def armor_per_level(self) -> float:
return self._data[StatsData].armorPerLevel
@property
def health_per_level(self) -> float:
return self._data[StatsData].healthPerLevel
@property
def attack_damage(self) -> float:
return self._data[StatsData].attackDamage
@property
def mana_per_level(self) -> float:
return self._data[StatsData].manaPerLevel
@property
def attack_speed(self) -> float:
return 0.625 / (1.0 + self._data[StatsData].attackSpeedOffset)
@property
def armor(self) -> float:
return self._data[StatsData].armor
@property
def health(self) -> float:
return self._data[StatsData].health
@property
def health_regen_per_level(self) -> float:
return self._data[StatsData].healthRegenPerLevel
@property
def magic_resist(self) -> float:
return self._data[StatsData].magicResist
@property
def attack_range(self) -> float:
return self._data[StatsData].attackRange
@property
def movespeed(self) -> float:
return self._data[StatsData].movespeed
@property
def attack_damage_per_level(self) -> float:
return self._data[StatsData].attackDamagePerLevel
@property
def mana_regen_per_level(self) -> float:
return self._data[StatsData].manaRegenPerLevel
@property
def mana(self) -> float:
return self._data[StatsData].mana
@property
def magic_resist_per_level(self) -> float:
return self._data[StatsData].magicResistPerLevel
@property
def critical_strike_chance(self) -> float:
return self._data[StatsData].criticalStrikeChance
@property
def mana_regen(self) -> float:
return self._data[StatsData].manaRegen
@property
def percent_attack_speed_per_level(self) -> float:
return self._data[StatsData].percentAttackSpeedPerLevel / 100.0
@property
def health_regen(self) -> float:
return self._data[StatsData].healthRegen
@property
def critical_strike_chance_per_level(self) -> float:
return self._data[StatsData].criticalStrikeChancePerLevel
[docs]class Info(CassiopeiaObject):
_data_types = {InfoData}
@property
def difficulty(self) -> int:
"""How Riot rates the difficulty of this champion."""
return self._data[InfoData].difficulty
@property
def attack(self) -> int:
"""How attack-oriented Riot rates this champion."""
return self._data[InfoData].attack
@property
def defense(self) -> int:
"""How defense-oriented Riot rates this champion."""
return self._data[InfoData].defense
@property
def magic(self) -> int:
"""How magic-oriented Riot rates this champion."""
return self._data[InfoData].magic
[docs]@searchable(
{
str: ["name", "key", "region", "platform", "locale", "tags"],
int: ["id"],
Region: ["region"],
Platform: ["platform"],
bool: ["free_to_play"],
}
)
class Champion(CassiopeiaGhost):
_data_types = (ChampionData, ChampionReleaseData, ChampionRatesData)
def __init__(
self,
*,
id: int = None,
name: str = None,
key: str = None,
region: Union[Region, str] = None,
version: str = None,
locale: str = None,
included_data: Set[str] = None
):
if included_data is None:
included_data = {"all"}
if locale is None and region is not None:
locale = Region(region).default_locale
kwargs = {"region": region, "included_data": included_data, "locale": locale}
if id is not None:
kwargs["id"] = id
if key is not None:
kwargs["key"] = key
if name is not None:
kwargs["name"] = name
if version is not None:
kwargs["version"] = version
super().__init__(**kwargs)
def __get_query__(self):
query = {
"region": self.region,
"platform": self.platform,
"version": self.version,
"locale": self.locale,
"includedData": self.included_data,
}
if hasattr(self._data[ChampionData], "id"):
query["id"] = self._data[ChampionData].id
if hasattr(self._data[ChampionData], "name"):
query["name"] = self._data[ChampionData].name
return query
def __eq__(self, other: "Champion"):
if not isinstance(other, Champion) or self.region != other.region:
return False
s = {}
o = {}
if hasattr(self._data[ChampionData], "id"):
s["id"] = self.id
if hasattr(other._data[ChampionData], "id"):
o["id"] = other.id
if hasattr(self._data[ChampionData], "name"):
s["name"] = self.name
if hasattr(other._data[ChampionData], "name"):
o["name"] = other.name
if any(s.get(key, "s") == o.get(key, "o") for key in s):
return True
else:
return self.id == other.id
def __str__(self):
region = self.region
id_ = "?"
name = "?"
if hasattr(self._data[ChampionData], "id"):
id_ = self.id
if hasattr(self._data[ChampionData], "name"):
name = self.name
return "Champion(name='{name}', id={id_}, region='{region}')".format(
name=name, id_=id_, region=region.value
)
__hash__ = CassiopeiaGhost.__hash__
[docs] def load(self, load_groups: Set = None) -> "Champion":
return super().load(load_groups=self._data_types)
def __load__(self, load_group: CoreData = None, load_groups: Set = None) -> None:
return super().__load__(load_group=load_group, load_groups=self._data_types)
# What do we do about params like this that can exist in both data objects?
# They will be set on both data objects always, so we can choose either one to return.
@lazy_property
def region(self) -> Region:
"""The region for this champion."""
return Region(self._data[ChampionData].region)
@lazy_property
def platform(self) -> Platform:
"""The platform for this champion."""
return self.region.platform
@property
def version(self) -> str:
"""The version for this champion."""
try:
return self._data[ChampionData].version
except AttributeError:
version = get_latest_version(region=self.region, endpoint="champion")
self(version=version)
return self._data[ChampionData].version
@property
def locale(self) -> str:
"""The locale for this champion."""
return self._data[ChampionData].locale or self.region.default_locale
@property
def included_data(self) -> Set[str]:
"""A set of tags to return additonal information for this champion when it's loaded."""
return self._data[ChampionData].includedData
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def id(self) -> int:
"""The champion's ID."""
return self._data[ChampionData].id
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def ally_tips(self) -> List[str]:
"""The tips for playing with this champion."""
return self._data[ChampionData].allyTips
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def enemy_tips(self) -> List[str]:
"""The tips for playing against this champion."""
return self._data[ChampionData].enemyTips
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def blurb(self) -> str:
"""A short blurb about this champion."""
return self._data[ChampionData].blurb
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def key(self) -> str:
"""The champion's key."""
return self._data[ChampionData].key
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def lore(self) -> str:
"""The champion's lore."""
return self._data[ChampionData].lore
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def name(self) -> str:
"""The champion's name."""
return self._data[ChampionData].name
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def resource(self) -> Resource:
"""The type of resource this champion uses."""
return Resource(self._data[ChampionData].resource)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def tags(self) -> List[str]:
"""The tags associated with this champion."""
return self._data[ChampionData].tags
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
def title(self) -> str:
"""The champion's title."""
return self._data[ChampionData].title
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def info(self) -> Info:
"""Info about this champion."""
return Info.from_data(self._data[ChampionData].info)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def recommended_itemsets(self) -> List[RecommendedItems]:
"""The champion's recommended itemsets."""
return SearchableList(
RecommendedItems.from_data(item, region=self.region)
for item in self._data[ChampionData].recommendedItemsets
)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def stats(self) -> Stats:
"""The champion's stats."""
return Stats.from_data(self._data[ChampionData].stats)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def skins(self) -> List[Skin]:
"""This champion's skins."""
skins = []
for skin in self._data[ChampionData].skins:
skins.append(Skin.from_data((skin)))
skins[-1]._Skin__champion_key = self.key
return SearchableList(skins)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def passive(self) -> Passive:
"""This champion's passive."""
return Passive.from_data(self._data[ChampionData].passive)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def spells(self) -> List[ChampionSpell]:
"""This champion's spells."""
keys = {0: "Q", 1: "W", 2: "E", 3: "R"}
spells = []
for i, spell in enumerate(self._data[ChampionData].spells):
spell.keyboard_key = keys[i]
spells.append(ChampionSpell.from_data(spell))
return SearchableList(spells)
@CassiopeiaGhost.property(ChampionData)
@ghost_load_on
@lazy
def image(self) -> Image:
"""The image information for this champion."""
image = Image.from_data(self._data[ChampionData].image)
image(version=self.version)
return image
@lazy_property
def sprite(self) -> Sprite:
return self.image.spriteInfo
@lazy_property
def free_to_play(self) -> bool:
"""Whether or not the champion is currently free to play."""
from ..champion import ChampionRotation
rotation = ChampionRotation(region=self.region)
for champ in rotation.free_champions:
if self.id == champ.id:
return True
else:
return False
@lazy_property
def free_to_play_new_players(self) -> bool:
"""Whether or not the champion is currently free to play for new players."""
from ..champion import ChampionRotation
rotation = ChampionRotation(region=self.region)
for champ in rotation.free_champions_for_new_players:
if self.id == champ.id:
return True
for champ in rotation.free_champions:
if self.id == champ.id:
return True
else:
return False
@CassiopeiaGhost.property(ChampionReleaseData)
@ghost_load_on
@lazy
def release_date(self) -> arrow.Arrow:
return arrow.get(self._data[ChampionReleaseData].releaseDate)
@CassiopeiaGhost.property(ChampionRatesData)
@ghost_load_on
@lazy
def play_rates(self) -> SearchableDictionary:
return self._data[ChampionRatesData].playRates
@CassiopeiaGhost.property(ChampionRatesData)
@ghost_load_on
@lazy
def win_rates(self) -> SearchableDictionary:
return self._data[ChampionRatesData].winRates
@CassiopeiaGhost.property(ChampionRatesData)
@ghost_load_on
@lazy
def ban_rates(self) -> SearchableDictionary:
return self._data[ChampionRatesData].banRates