Source code for cassiopeia.core.staticdata.champion

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