├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── hearthstone ├── __init__.py ├── bountyxml.py ├── cardxml.py ├── dbf.py ├── deckstrings.py ├── entities.py ├── enums.py ├── mercenaryxml.py ├── stringsfile.py ├── types.py ├── utils │ └── __init__.py └── xmlutils.py ├── scripts └── dump_reprints.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── res │ └── DECK_RULESET_RULE_SUBSET.xml ├── test_bountyxml.py ├── test_cardxml.py ├── test_dbf.py ├── test_deckstrings.py ├── test_entities.py ├── test_enums.py ├── test_mercenaryxml.py ├── test_stringsfile.py └── test_utils.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = tab 9 | quote_type = double 10 | insert_final_newline = true 11 | tab_width = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.py] 15 | spaces_around_brackets = none 16 | spaces_around_operators = true 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up Python 3.10 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: '3.10' 15 | - name: Install dependencies 16 | run: | 17 | pip install --upgrade pip setuptools types-setuptools wheel 18 | pip install tox 19 | - name: Run tox 20 | run: tox 21 | release: 22 | name: Release 23 | needs: [test] 24 | runs-on: ubuntu-latest 25 | if: startsWith(github.ref, 'refs/tags') 26 | permissions: 27 | # required to authenticate for the PyPi upload below 28 | id-token: write 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Set up Python 3.10 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: '3.10' 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install wheel 39 | - name: Build 40 | run: python setup.py sdist bdist_wheel 41 | - name: Upload to pypi 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | hearthstone.egg-info 4 | coverage.xml 5 | venv 6 | 7 | *.pyc 8 | .tox 9 | .vscode 10 | .env 11 | .coverage 12 | .idea 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jerome Leclanche 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-hearthstone 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/hearthsim/python-hearthstone/ci.yml?branch=master)](https://github.com/HearthSim/python-hearthstone/actions/workflows/ci.yml) 4 | [![PyPI](https://img.shields.io/pypi/v/hearthstone.svg)](https://pypi.org/project/hearthstone/) 5 | 6 | A Hearthstone Python library containing: 7 | 8 | * A CardDefs.xml parser (`hearthstone.cardxml`) 9 | * A DbfXml parser (`hearthstone.dbf`) 10 | * A deck code encoder and decoder (`hearthstone.deckstrings`) 11 | * Hearthstone enums as IntEnum (`hearthstone.enums`) 12 | 13 | The CardDefs.xml data for the latest build can optionally be installed from the 14 | [python-hearthstone-data repository](https://github.com/HearthSim/python-hearthstone-data) 15 | or on PyPI with `pip install hearthstone_data`. Otherwise, they will be download at runtime. 16 | 17 | 18 | ## Requirements 19 | 20 | * Python 3.6+ 21 | * lxml 22 | 23 | ## Installation 24 | 25 | * To install from PyPI: `pip install hearthstone` 26 | 27 | 28 | ## License 29 | 30 | This project is licensed under the MIT license. The full license text is 31 | available in the LICENSE file. 32 | 33 | 34 | ## Community 35 | 36 | This is a [HearthSim](https://hearthsim.info) project. 37 | Join the HearthSim Developer community [on Discord](https://discord.gg/hearthsim-devs). 38 | -------------------------------------------------------------------------------- /hearthstone/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from importlib.metadata import version 3 | 4 | __version__ = version("hearthstone") 5 | except ImportError: 6 | import pkg_resources 7 | 8 | __version__ = pkg_resources.require("hearthstone")[0].version 9 | -------------------------------------------------------------------------------- /hearthstone/bountyxml.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from typing import Any, Callable, Dict, Iterator, Tuple 3 | 4 | from .enums import Role 5 | from .utils import ElementTree 6 | from .xmlutils import download_to_tempfile_retry 7 | 8 | 9 | class BountyXML: 10 | 11 | @classmethod 12 | def from_xml(cls, xml): 13 | self = cls(int(xml.attrib["ID"])) 14 | self.is_heroic = xml.attrib["is_heroic"].lower() == "true" 15 | self.level = int(xml.attrib["level"]) 16 | 17 | boss = xml.find("Boss") 18 | self.boss_dbf_id = int(boss.attrib["CardID"]) 19 | self.boss_role = Role(int(boss.attrib["role"])) 20 | 21 | boss_names = boss.find("Name") 22 | for loc_element in boss_names: 23 | self._localized_boss_names[loc_element.tag] = loc_element.text 24 | 25 | region = xml.find("Set") 26 | self.region_id = int(region.attrib["ID"]) 27 | for loc_element in region: 28 | self._localized_region_names[loc_element.tag] = loc_element.text 29 | 30 | rewards = xml.findall("Reward") 31 | for reward in rewards: 32 | self.reward_mercenary_dbf_ids.add(int(reward.attrib["MercenaryID"])) 33 | 34 | return self 35 | 36 | def __init__(self, bounty_id, locale="enUS"): 37 | self.id = bounty_id 38 | self.boss_dbf_id = 0 39 | self.boss_role = Role.INVALID 40 | self.is_heroic = False 41 | self.level = 0 42 | self.region_id = 0 43 | self.reward_mercenary_dbf_ids = set() 44 | 45 | self.locale = locale 46 | 47 | self._localized_boss_names = {} 48 | self._localized_region_names = {} 49 | 50 | @property 51 | def boss_name(self): 52 | return self._localized_boss_names.get(self.locale, "") 53 | 54 | @property 55 | def region_name(self): 56 | return self._localized_region_names.get(self.locale, "") 57 | 58 | 59 | bounty_cache: Dict[Tuple[str, str], Tuple[Dict[int, BountyXML], Any]] = {} 60 | 61 | 62 | XML_URL = "https://api.hearthstonejson.com/v1/latest/BountyDefs.xml" 63 | 64 | 65 | def _bootstrap_from_web(parse: Callable[[Iterator[Tuple[str, Any]]], None]): 66 | with tempfile.TemporaryFile(mode="rb+") as fp: 67 | if download_to_tempfile_retry(XML_URL, fp): 68 | fp.flush() 69 | fp.seek(0) 70 | 71 | parse(ElementTree.iterparse(fp, events=("start", "end",))) 72 | 73 | 74 | def _bootstrap_from_library(parse: Callable[[Iterator[Tuple[str, Any]]], None], path=None): 75 | from hearthstone_data import get_bountydefs_path 76 | 77 | if path is None: 78 | path = get_bountydefs_path() 79 | 80 | with open(path, "rb") as f: 81 | parse(ElementTree.iterparse(f, events=("start", "end",))) 82 | 83 | 84 | def load(path=None, locale="enUS"): 85 | cache_key = (path, locale) 86 | if cache_key not in bounty_cache: 87 | db = {} 88 | 89 | def parse(context: Iterator[Tuple[str, Any]]): 90 | nonlocal db 91 | root = None 92 | for action, elem in context: 93 | if action == "start" and elem.tag == "BountyDefs": 94 | root = elem 95 | continue 96 | 97 | if action == "end" and elem.tag == "Bounty": 98 | bounty = BountyXML.from_xml(elem) 99 | bounty.locale = locale 100 | db[bounty.id] = bounty 101 | 102 | elem.clear() # type: ignore 103 | root.clear() # type: ignore 104 | 105 | if path is None: 106 | # Check if the hearthstone_data package exists locally 107 | has_lib = True 108 | try: 109 | import hearthstone_data # noqa: F401 110 | except ImportError: 111 | has_lib = False 112 | 113 | if not has_lib: 114 | _bootstrap_from_web(parse) 115 | 116 | if not db: 117 | _bootstrap_from_library(parse, path=path) 118 | 119 | bounty_cache[cache_key] = (db, None) 120 | 121 | return bounty_cache[cache_key] 122 | -------------------------------------------------------------------------------- /hearthstone/cardxml.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from typing import Any, Callable, Iterator, Optional, Sequence, Tuple 3 | 4 | from .enums import ( 5 | CardClass, CardSet, CardType, Faction, GameTag, 6 | MultiClassGroup, Race, Rarity, Role, SpellSchool 7 | ) 8 | from .utils import ElementTree 9 | from .xmlutils import download_to_tempfile_retry 10 | 11 | 12 | LOCALIZED_TAGS = [ 13 | GameTag.CARDNAME, GameTag.CARDTEXT_INHAND, GameTag.FLAVORTEXT, 14 | GameTag.HOW_TO_EARN, GameTag.HOW_TO_EARN_GOLDEN, 15 | GameTag.CardTextInPlay, GameTag.TARGETING_ARROW_TEXT, 16 | ] 17 | 18 | STRING_TAGS = [GameTag.ARTISTNAME, GameTag.LocalizationNotes] 19 | 20 | 21 | def prop(tag, cast=int): 22 | def _func(self): 23 | value = self.tags.get(tag, 0) 24 | try: 25 | return cast(value) 26 | except ValueError: 27 | # The enum value is most likely just missing 28 | return value 29 | return property(_func) 30 | 31 | 32 | def _locstring(tag): 33 | def _func(self): 34 | value = self.strings[tag] 35 | if self.locale in value: 36 | return value[self.locale] 37 | return value.get("enUS", "") 38 | return property(_func) 39 | 40 | 41 | def _make_tag_element(element, tagname, tag, value): 42 | e = ElementTree.SubElement(element, tagname, enumID=str(int(tag))) 43 | if not isinstance(tag, GameTag): 44 | try: 45 | tag = GameTag(tag) 46 | name = tag.name 47 | value = str(int(value)) 48 | except ValueError: 49 | name = str(value) 50 | value = str(value) 51 | else: 52 | name = tag.name 53 | value = str(int(value)) 54 | 55 | e.attrib["name"] = name 56 | e.attrib["type"] = "Int" 57 | e.attrib["value"] = value 58 | 59 | return e 60 | 61 | 62 | def _unpack_tag_xml(e): 63 | value = int(e.attrib["enumID"]) 64 | try: 65 | tag = GameTag(value) 66 | except ValueError: 67 | tag = value 68 | type = e.attrib.get("type", "Int") 69 | value = int(e.attrib.get("value") or 0) 70 | if type == "Bool": 71 | value = bool(value) 72 | return tag, type, value 73 | 74 | 75 | class CardXML: 76 | @classmethod 77 | def from_xml(cls, xml): 78 | id = xml.attrib["CardID"] 79 | self = cls(id) 80 | self.dbf_id = int(xml.attrib.get("ID", 0)) 81 | 82 | for e in xml.findall("./Tag"): 83 | tag, type, value = _unpack_tag_xml(e) 84 | if type == "String": 85 | self.strings[tag] = e.text 86 | elif type == "LocString": 87 | for loc_element in e: 88 | self.strings[tag][loc_element.tag] = loc_element.text 89 | else: 90 | if tag == GameTag.HERO_POWER: 91 | self.hero_power = e.attrib.get("cardID") 92 | self.tags[tag] = value 93 | 94 | for e in xml.findall("./ReferencedTag"): 95 | tag, type, value = _unpack_tag_xml(e) 96 | self.referenced_tags[tag] = value 97 | 98 | if self.hero_power is None and self.tags.get(GameTag.HERO_POWER): 99 | i = int(GameTag.HERO_POWER) 100 | t = xml.findall('./Tag[@enumID="%i"]' % (i)) 101 | if t is not None: 102 | self.hero_power = t[0].attrib.get("cardID") 103 | 104 | return self 105 | 106 | def __init__(self, id, locale="enUS"): 107 | self.card_id = self.id = id 108 | self.dbf_id = 0 109 | self.version = 2 110 | self.tags = {} 111 | self.hero_power = None 112 | self.referenced_tags = {} 113 | 114 | self.locale = locale 115 | 116 | self.strings = { 117 | GameTag.CARDNAME: {}, 118 | GameTag.CARDTEXT_INHAND: {}, 119 | GameTag.FLAVORTEXT: {}, 120 | GameTag.HOW_TO_EARN: {}, 121 | GameTag.HOW_TO_EARN_GOLDEN: {}, 122 | GameTag.CardTextInPlay: {}, 123 | GameTag.TARGETING_ARROW_TEXT: {}, 124 | GameTag.ARTISTNAME: "", 125 | GameTag.LocalizationNotes: "", 126 | } 127 | 128 | def __str__(self): 129 | return self.name 130 | 131 | def __repr__(self): 132 | return "<%s: %r>" % (self.id, self.name) 133 | 134 | def to_xml( 135 | self, 136 | tags: Optional[Sequence[GameTag]] = None, 137 | locales: Optional[Sequence[str]] = None 138 | ): 139 | ret = ElementTree.Element("Entity", CardID=self.id, ID=str(self.dbf_id)) 140 | if self.version: 141 | ret.attrib["version"] = str(self.version) 142 | 143 | for tag in LOCALIZED_TAGS: 144 | if tags is not None and tag not in tags: 145 | continue 146 | 147 | value = self.strings[tag] 148 | if value: 149 | e = ElementTree.SubElement(ret, "Tag", enumID=str(int(tag)), name=tag.name) 150 | e.attrib["type"] = "LocString" 151 | for locale, localized_value in sorted(value.items()): 152 | if locales is not None and locale not in locales: 153 | continue 154 | 155 | if localized_value: 156 | loc_element = ElementTree.SubElement(e, locale) 157 | loc_element.text = str(localized_value) 158 | 159 | for tag in STRING_TAGS: 160 | if tags is not None and tag not in tags: 161 | continue 162 | 163 | value = self.strings[tag] 164 | if value: 165 | e = ElementTree.SubElement(ret, "Tag", enumID=str(int(tag)), name=tag.name) 166 | e.attrib["type"] = "String" 167 | e.text = value 168 | 169 | for tag, value in sorted(self.tags.items()): 170 | if tags is not None and tag not in tags: 171 | continue 172 | 173 | if value: 174 | e = _make_tag_element(ret, "Tag", tag, value) 175 | 176 | if tag == GameTag.HERO_POWER and self.hero_power: 177 | e.attrib["type"] = "Card" 178 | e.attrib["cardID"] = self.hero_power 179 | 180 | for tag, value in sorted(self.referenced_tags.items()): 181 | if tags and tag not in tags: 182 | continue 183 | 184 | e = _make_tag_element(ret, "ReferencedTag", tag, value) 185 | 186 | return ret 187 | 188 | @property 189 | def craftable(self): 190 | if isinstance(self.card_set, CardSet) and not self.card_set.craftable: 191 | return False 192 | if not self.type.craftable: 193 | return False 194 | if not self.rarity.craftable: 195 | return False 196 | return True 197 | 198 | @property 199 | def crafting_costs(self): 200 | if not self.craftable: 201 | return 0, 0 202 | return self.rarity.crafting_costs 203 | 204 | @property 205 | def disenchant_costs(self): 206 | if not self.craftable: 207 | return 0, 0 208 | return self.rarity.disenchant_costs 209 | 210 | @property 211 | def max_count_in_deck(self): 212 | """ 213 | The maximum amount of times the card can be present in a deck. 214 | """ 215 | if self.rarity == Rarity.LEGENDARY: 216 | return 1 217 | return 2 218 | 219 | @property 220 | def quest_reward(self): 221 | from .utils import QUEST_REWARDS 222 | return QUEST_REWARDS.get(self.card_id, "") 223 | 224 | ## 225 | # Localized values 226 | 227 | name = _locstring(GameTag.CARDNAME) 228 | description = _locstring(GameTag.CARDTEXT_INHAND) 229 | flavortext = _locstring(GameTag.FLAVORTEXT) 230 | how_to_earn = _locstring(GameTag.HOW_TO_EARN) 231 | how_to_earn_golden = _locstring(GameTag.HOW_TO_EARN_GOLDEN) 232 | playtext = _locstring(GameTag.CardTextInPlay) 233 | targeting_arrow_text = _locstring(GameTag.TARGETING_ARROW_TEXT) 234 | 235 | @property 236 | def artist(self): 237 | return self.strings[GameTag.ARTISTNAME] 238 | 239 | @property 240 | def localization_notes(self): 241 | return self.strings[GameTag.LocalizationNotes] 242 | 243 | @property 244 | def classes(self): 245 | ret = [] 246 | multiclass = self.multiple_classes 247 | if not multiclass: 248 | ret.append(self.card_class) 249 | else: 250 | i = 1 251 | while multiclass != 0: 252 | if (multiclass & 1) == 1 and i in CardClass._value2member_map_: 253 | ret.append(CardClass(i)) 254 | multiclass >>= 1 255 | i += 1 256 | 257 | return ret 258 | 259 | @property 260 | def races(self): 261 | ret = [] 262 | 263 | for tag, value in self.tags.items(): 264 | if tag == GameTag.CARDRACE: 265 | ret.append(Race(value)) 266 | continue 267 | 268 | potential_race_tag = Race.get_race_for_game_tag(tag) 269 | if potential_race_tag is not None: 270 | ret.append(Race(potential_race_tag)) 271 | 272 | return sorted(ret, key=lambda r: r.text_order) 273 | 274 | @property 275 | def english_name(self): 276 | return self.strings[GameTag.CARDNAME].get("enUS", "") 277 | 278 | @property 279 | def english_description(self): 280 | return self.strings[GameTag.CARDTEXT_INHAND].get("enUS", "") 281 | 282 | def is_functional_duplicate_of(self, other): 283 | """ 284 | This method can be used to check whether two cards are functionally identical from a 285 | Constructed gameplay perspective. For example, if cards have the same English name, 286 | description, stats and races, they're probably the same. However, if for example the 287 | mana costs differ, the card is different because one card is strictly better than 288 | another one and can be played in different circumstances. 289 | You can use this method catch cases where cards are reprinted in different sets and 290 | may otherwise appear as duplicates (e.g. by looking at 291 | GameTag.DECK_RULE_COUNT_AS_COPY_OF_CARD_ID). 292 | """ 293 | if not isinstance(other, CardXML): 294 | raise ValueError("other must be a CardXML instance") 295 | 296 | english_name = self.english_name 297 | return ( 298 | english_name and 299 | other.english_name == english_name and 300 | other.description == self.description and 301 | other.cost == self.cost and 302 | other.health == self.health and 303 | other.atk == self.atk and 304 | other.type == self.type and 305 | set(other.races) == set(self.races) 306 | ) 307 | 308 | ## 309 | # Enums 310 | 311 | card_class = prop(GameTag.CLASS, CardClass) 312 | card_set = prop(GameTag.CARD_SET, CardSet) 313 | faction = prop(GameTag.FACTION, Faction) 314 | race = prop(GameTag.CARDRACE, Race) 315 | rarity = prop(GameTag.RARITY, Rarity) 316 | type = prop(GameTag.CARDTYPE, CardType) 317 | multi_class_group = prop(GameTag.MULTI_CLASS_GROUP, MultiClassGroup) 318 | spell_school = prop(GameTag.SPELL_SCHOOL, SpellSchool) 319 | role = prop(GameTag.LETTUCE_ROLE, Role) 320 | 321 | ## 322 | # Bools 323 | 324 | adapt = prop(GameTag.ADAPT, bool) 325 | appear_functionally_dead = prop(GameTag.APPEAR_FUNCTIONALLY_DEAD, bool) 326 | autoattack = prop(GameTag.AUTOATTACK, bool) 327 | can_summon_maxplusone_minion = prop(GameTag.CAN_SUMMON_MAXPLUSONE_MINION, bool) 328 | cant_be_attacked = prop(GameTag.CANT_BE_ATTACKED, bool) 329 | cant_be_fatigued = prop(GameTag.CANT_BE_FATIGUED, bool) 330 | collectible = prop(GameTag.COLLECTIBLE, bool) 331 | colossal = prop(GameTag.COLOSSAL, bool) 332 | battlecry = prop(GameTag.BATTLECRY, bool) 333 | choose_one = prop(GameTag.CHOOSE_ONE, bool) 334 | combo = prop(GameTag.COMBO, bool) 335 | corrupt = prop(GameTag.CORRUPT, bool) 336 | deathrattle = prop(GameTag.DEATHRATTLE, bool) 337 | discover = prop(GameTag.DISCOVER, bool) 338 | divine_shield = prop(GameTag.DIVINE_SHIELD, bool) 339 | double_spelldamage_bonus = prop(GameTag.RECEIVES_DOUBLE_SPELLDAMAGE_BONUS, bool) 340 | dredge = prop(GameTag.DREDGE, bool) 341 | echo = prop(GameTag.ECHO, bool) 342 | elite = prop(GameTag.ELITE, bool) 343 | elusive = prop(GameTag.ELUSIVE, bool) 344 | evil_glow = prop(GameTag.EVIL_GLOW, bool) 345 | forge = prop(GameTag.FORGE, bool) 346 | forgetful = prop(GameTag.FORGETFUL, bool) 347 | ghostly = prop(GameTag.GHOSTLY, bool) 348 | hide_health = prop(GameTag.HIDE_HEALTH, bool) 349 | hide_stats = prop(GameTag.HIDE_STATS, bool) 350 | hide_cost = prop(GameTag.HIDE_COST, bool) 351 | immune = prop(GameTag.IMMUNE, bool) 352 | inspire = prop(GameTag.INSPIRE, bool) 353 | jade_golem = prop(GameTag.JADE_GOLEM, bool) 354 | lifesteal = prop(GameTag.LIFESTEAL, bool) 355 | magnetic = prop(GameTag.MODULAR, bool) 356 | miniaturize = prop(GameTag.MINIATURIZE, bool) 357 | one_turn_effect = prop(GameTag.TAG_ONE_TURN_EFFECT, bool) 358 | outcast = prop(GameTag.OUTCAST, bool) 359 | overheal = prop(GameTag.OVERHEAL, bool) 360 | overkill = prop(GameTag.OVERKILL, bool) 361 | poisonous = prop(GameTag.POISONOUS, bool) 362 | quest = prop(GameTag.QUEST, bool) 363 | reborn = prop(GameTag.REBORN, bool) 364 | ritual = prop(GameTag.RITUAL, bool) 365 | rush = prop(GameTag.RUSH, bool) 366 | secret = prop(GameTag.SECRET, bool) 367 | sidequest = prop(GameTag.SIDEQUEST, bool) 368 | spare_part = prop(GameTag.SPARE_PART, bool) 369 | spellburst = prop(GameTag.SPELLBURST, bool) 370 | start_of_game = prop(GameTag.START_OF_GAME, bool) 371 | taunt = prop(GameTag.TAUNT, bool) 372 | titan = prop(GameTag.TITAN, bool) 373 | topdeck = prop(GameTag.TOPDECK, bool) 374 | tradeable = prop(GameTag.TRADEABLE, bool) 375 | twinspell = prop(GameTag.TWINSPELL, bool) 376 | untouchable = prop(GameTag.UNTOUCHABLE, bool) 377 | venomous = prop(GameTag.VENOMOUS, bool) 378 | 379 | ## 380 | # Tags 381 | 382 | armor = prop(GameTag.ARMOR) 383 | atk = prop(GameTag.ATK) 384 | avenge = prop(GameTag.AVENGE) 385 | durability = prop(GameTag.DURABILITY) 386 | cost = prop(GameTag.COST) 387 | health = prop(GameTag.HEALTH) 388 | manathirst = prop(GameTag.MANATHIRST) 389 | windfury = prop(GameTag.WINDFURY) 390 | quest_progress_total = prop(GameTag.QUEST_PROGRESS_TOTAL) 391 | cooldown = prop(GameTag.LETTUCE_COOLDOWN_CONFIG) 392 | 393 | ## 394 | # Auto-guessed extras 395 | 396 | overload = prop(GameTag.OVERLOAD) 397 | heropower_damage = prop(GameTag.HEROPOWER_DAMAGE) 398 | spell_damage = prop(GameTag.SPELLPOWER) 399 | 400 | ## 401 | # Misc 402 | 403 | multiple_classes = prop(GameTag.MULTIPLE_CLASSES) 404 | script_data_num_1 = prop(GameTag.TAG_SCRIPT_DATA_NUM_1) 405 | 406 | # Faction bools - deprecated, use multi_class_group instead 407 | grimy_goons = prop(GameTag.GRIMY_GOONS, bool) 408 | jade_lotus = prop(GameTag.JADE_LOTUS, bool) 409 | kabal = prop(GameTag.KABAL, bool) 410 | 411 | 412 | cardid_cache: dict = {} 413 | dbf_cache: dict = {} 414 | 415 | 416 | XML_URL = "https://api.hearthstonejson.com/v1/latest/CardDefs.xml" 417 | 418 | 419 | def _bootstrap_from_web(parse: Callable[[Iterator[Tuple[str, Any]]], None], url=None): 420 | if url is None: 421 | url = XML_URL 422 | 423 | with tempfile.TemporaryFile(mode="rb+") as fp: 424 | if download_to_tempfile_retry(url, fp): 425 | fp.flush() 426 | fp.seek(0) 427 | 428 | parse(ElementTree.iterparse(fp, events=("start", "end",))) 429 | 430 | 431 | def _bootstrap_from_library(parse: Callable[[Iterator[Tuple[str, Any]]], None], path=None): 432 | from hearthstone_data import get_carddefs_path 433 | 434 | if path is None: 435 | path = get_carddefs_path() 436 | 437 | with open(path, "rb") as f: 438 | parse(ElementTree.iterparse(f, events=("start", "end",))) 439 | 440 | 441 | def _load(path, locale, cache, attr, url=None): 442 | cache_key = (path, locale) 443 | if cache_key not in cache: 444 | db = {} 445 | 446 | def parse(context: Iterator[Tuple[str, Any]]): 447 | nonlocal db 448 | root = None 449 | for action, elem in context: 450 | if action == "start" and elem.tag == "CardDefs": 451 | root = elem 452 | continue 453 | 454 | if action == "end" and elem.tag == "Entity": 455 | card = CardXML.from_xml(elem) 456 | card.locale = locale 457 | db[getattr(card, attr)] = card 458 | 459 | elem.clear() # type: ignore 460 | root.clear() # type: ignore 461 | 462 | if path is None: 463 | # Check if the hearthstone_data package exists locally 464 | has_lib = True 465 | try: 466 | import hearthstone_data # noqa: F401 467 | except ImportError: 468 | has_lib = False 469 | 470 | if not has_lib: 471 | _bootstrap_from_web(parse, url=url) 472 | 473 | if not db: 474 | _bootstrap_from_library(parse, path=path) 475 | 476 | cache[cache_key] = (db, None) 477 | 478 | return cache[cache_key] 479 | 480 | 481 | def load(path=None, locale="enUS", url=None): 482 | return _load(path, locale, cardid_cache, "id", url) 483 | 484 | 485 | def load_dbf(path=None, locale="enUS", url=None): 486 | return _load(path, locale, dbf_cache, "dbf_id", url) 487 | -------------------------------------------------------------------------------- /hearthstone/dbf.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | 4 | from .enums import Locale 5 | from .utils import ElementTree 6 | 7 | 8 | class Dbf: 9 | @classmethod 10 | def load(cls, filename): 11 | ret = cls() 12 | with open(filename, "r") as f: 13 | ret.populate(f) 14 | return ret 15 | 16 | def __init__(self): 17 | self.name = None 18 | self.records = [] 19 | self.columns = OrderedDict() 20 | self.source_fingerprint = None 21 | 22 | def __repr__(self): 23 | return "<%s: %s>" % (self.__class__.__name__, self.name) 24 | 25 | def _deserialize_record(self, element): 26 | ret = {} 27 | for field in element.findall("Field"): 28 | colname = field.attrib["column"] 29 | coltype = self.columns[colname] 30 | ret[colname] = self._deserialize_value(field, coltype) 31 | 32 | return ret 33 | 34 | def _deserialize_value(self, element, coltype): 35 | if element.text is None: 36 | return 37 | if coltype in ("Int", "Long", "ULong"): 38 | return int(element.text) 39 | elif coltype == "Float": 40 | return float(element.text) 41 | elif coltype == "Bool": 42 | return element.text == "True" 43 | elif coltype in ("String", "AssetPath"): 44 | return element.text 45 | elif coltype == "LocString": 46 | return {e.tag: e.text for e in element} 47 | raise NotImplementedError("Unknown DBF Data Type: %r" % (coltype)) 48 | 49 | def populate(self, file): 50 | _xml = ElementTree.parse(file) 51 | self.name = _xml.getroot().attrib.get("name", "") 52 | for fingerprint in _xml.findall("SourceFingerprint"): 53 | self.source_fingerprint = fingerprint.text 54 | 55 | for column in _xml.findall("Column"): 56 | self.columns[column.attrib["name"]] = column.attrib["type"] 57 | 58 | self.records = [self._deserialize_record(e) for e in _xml.findall("Record")] 59 | 60 | def object_to_xml_column_name(self, name): 61 | if name.startswith("m_"): 62 | name = name[2:] 63 | name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) 64 | return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).upper() 65 | 66 | def populate_from_unity_object(self, obj): 67 | d = obj.read() 68 | self.name = d["m_Name"] 69 | colnames = {} 70 | coltypes = {} 71 | 72 | # m_Records > Array > data 73 | record_tree = obj.type_tree.children[4].children[0].children[1] 74 | for field in record_tree.children: 75 | coltype = { 76 | "SInt64": "Long", 77 | "UInt64": "ULong", 78 | "int": "Int", 79 | "double": "Float", 80 | "float": "Float", 81 | "string": "String", 82 | "DbfLocValue": "LocString", 83 | "UInt8": "Bool" 84 | }[field.type] 85 | colname = field.name 86 | if colname.startswith("m_"): 87 | colname = colname[2:] 88 | colname = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", colname) 89 | colname = re.sub("([a-z0-9])([A-Z])", r"\1_\2", colname).upper() 90 | colnames[field.name] = colname 91 | coltypes[field.name] = coltype 92 | self.columns[colname] = coltype 93 | 94 | records = d["Records"] 95 | for record in records: 96 | r = {} 97 | for name, val in record.items(): 98 | colname = colnames[name] 99 | coltype = coltypes[name] 100 | if coltype == "LocString": 101 | locStrings = zip(val["m_locales"], val["m_locValues"]) 102 | r[colname] = dict((Locale(loc).name, s) for loc, s in locStrings) 103 | elif coltype == "Bool": 104 | r[colname] = val != 0 105 | else: 106 | r[colname] = val 107 | self.records.append(r) 108 | 109 | def _to_xml(self): 110 | root = ElementTree.Element("Dbf") 111 | 112 | if self.name is not None: 113 | root.attrib["name"] = self.name 114 | 115 | if self.source_fingerprint is not None: 116 | e = ElementTree.Element("SourceFingerprint") 117 | root.append(e) 118 | e.text = self.source_fingerprint 119 | 120 | for column, type in self.columns.items(): 121 | e = ElementTree.Element("Column") 122 | root.append(e) 123 | e.attrib["name"] = column 124 | e.attrib["type"] = type 125 | 126 | for record in self.records: 127 | e = ElementTree.Element("Record") 128 | root.append(e) 129 | for column, type in self.columns.items(): 130 | field = ElementTree.Element("Field") 131 | e.append(field) 132 | field.attrib["column"] = column 133 | value = record[column] 134 | if value is None: 135 | continue 136 | 137 | if type == "LocString": 138 | locales = sorted(value.keys()) 139 | # Always have enUS as first item 140 | if "enUS" in locales: 141 | locales.insert(0, locales.pop(locales.index("enUS"))) 142 | for locale in locales: 143 | eloc = ElementTree.Element(locale) 144 | field.append(eloc) 145 | eloc.text = value[locale] 146 | else: 147 | field.text = str(record[column]) 148 | 149 | return root 150 | 151 | def to_xml(self, encoding="utf-8"): 152 | root = self._to_xml() 153 | return ElementTree.tostring(root, encoding=encoding) 154 | -------------------------------------------------------------------------------- /hearthstone/deckstrings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Blizzard Deckstring format support 3 | """ 4 | 5 | import base64 6 | from io import BytesIO 7 | from typing import IO, List, Optional, Sequence, Tuple 8 | 9 | from .enums import FormatType 10 | 11 | 12 | DECKSTRING_VERSION = 1 13 | 14 | 15 | CardList = List[int] 16 | CardIncludeList = List[Tuple[int, int]] 17 | SideboardList = List[Tuple[int, int, int]] 18 | 19 | 20 | def _read_varint(stream: IO) -> int: 21 | shift = 0 22 | result = 0 23 | while True: 24 | c = stream.read(1) 25 | if c == "": 26 | raise EOFError("Unexpected EOF while reading varint") 27 | i = ord(c) 28 | result |= (i & 0x7f) << shift 29 | shift += 7 30 | if not (i & 0x80): 31 | break 32 | 33 | return result 34 | 35 | 36 | def _write_varint(stream: IO, i: int) -> int: 37 | buf = b"" 38 | while True: 39 | towrite = i & 0x7f 40 | i >>= 7 41 | if i: 42 | buf += bytes((towrite | 0x80, )) 43 | else: 44 | buf += bytes((towrite, )) 45 | break 46 | 47 | return stream.write(buf) 48 | 49 | 50 | class Deck: 51 | @classmethod 52 | def from_deckstring(cls, deckstring: str) -> "Deck": 53 | instance = cls() 54 | ( 55 | instance.cards, 56 | instance.heroes, 57 | instance.format, 58 | instance.sideboards, 59 | ) = parse_deckstring(deckstring) 60 | return instance 61 | 62 | def __init__(self): 63 | self.cards: CardIncludeList = [] 64 | self.sideboards: SideboardList = [] 65 | self.heroes: CardList = [] 66 | self.format: FormatType = FormatType.FT_UNKNOWN 67 | 68 | @property 69 | def as_deckstring(self) -> str: 70 | return write_deckstring(self.cards, self.heroes, self.format, self.sideboards) 71 | 72 | def get_dbf_id_list(self) -> CardIncludeList: 73 | return sorted(self.cards, key=lambda x: x[0]) 74 | 75 | def get_sideboard_dbf_id_list(self) -> SideboardList: 76 | return sorted(self.sideboards, key=lambda x: x[0]) 77 | 78 | 79 | def trisort_cards(cards: Sequence[tuple]) -> Tuple[ 80 | List[tuple], List[tuple], List[tuple] 81 | ]: 82 | cards_x1: List[tuple] = [] 83 | cards_x2: List[tuple] = [] 84 | cards_xn: List[tuple] = [] 85 | 86 | for card_elem in cards: 87 | sideboard_owner = None 88 | if len(card_elem) == 3: 89 | # Sideboard 90 | cardid, count, sideboard_owner = card_elem 91 | else: 92 | cardid, count = card_elem 93 | 94 | if count == 1: 95 | list = cards_x1 96 | elif count == 2: 97 | list = cards_x2 98 | else: 99 | list = cards_xn 100 | 101 | if len(card_elem) == 3: 102 | list.append((cardid, count, sideboard_owner)) 103 | else: 104 | list.append((cardid, count)) 105 | 106 | return cards_x1, cards_x2, cards_xn 107 | 108 | 109 | def parse_deckstring(deckstring) -> ( 110 | Tuple[CardIncludeList, CardList, FormatType, SideboardList] 111 | ): 112 | decoded = base64.b64decode(deckstring) 113 | data = BytesIO(decoded) 114 | 115 | # Header section 116 | 117 | if data.read(1) != b"\0": 118 | raise ValueError("Invalid deckstring") 119 | 120 | version = _read_varint(data) 121 | if version != DECKSTRING_VERSION: 122 | raise ValueError("Unsupported deckstring version %r" % (version)) 123 | 124 | format = _read_varint(data) 125 | try: 126 | format = FormatType(format) 127 | except ValueError: 128 | raise ValueError("Unsupported FormatType in deckstring %r" % (format)) 129 | 130 | # Heroes section 131 | 132 | heroes: CardList = [] 133 | num_heroes = _read_varint(data) 134 | for i in range(num_heroes): 135 | heroes.append(_read_varint(data)) 136 | heroes.sort() 137 | 138 | # Cards section 139 | 140 | cards: CardIncludeList = [] 141 | 142 | num_cards_x1 = _read_varint(data) 143 | for i in range(num_cards_x1): 144 | card_id = _read_varint(data) 145 | cards.append((card_id, 1)) 146 | 147 | num_cards_x2 = _read_varint(data) 148 | for i in range(num_cards_x2): 149 | card_id = _read_varint(data) 150 | cards.append((card_id, 2)) 151 | 152 | num_cards_xn = _read_varint(data) 153 | for i in range(num_cards_xn): 154 | card_id = _read_varint(data) 155 | count = _read_varint(data) 156 | cards.append((card_id, count)) 157 | 158 | cards.sort() 159 | 160 | # Sideboards section 161 | 162 | sideboards = [] 163 | 164 | has_sideboards = data.read(1) == b"\1" 165 | 166 | if has_sideboards: 167 | num_sideboards_x1 = _read_varint(data) 168 | for i in range(num_sideboards_x1): 169 | card_id = _read_varint(data) 170 | sideboard_owner = _read_varint(data) 171 | sideboards.append((card_id, 1, sideboard_owner)) 172 | 173 | num_sideboards_x2 = _read_varint(data) 174 | for i in range(num_sideboards_x2): 175 | card_id = _read_varint(data) 176 | sideboard_owner = _read_varint(data) 177 | sideboards.append((card_id, 2, sideboard_owner)) 178 | 179 | num_sideboards_xn = _read_varint(data) 180 | for i in range(num_sideboards_xn): 181 | card_id = _read_varint(data) 182 | count = _read_varint(data) 183 | sideboard_owner = _read_varint(data) 184 | sideboards.append((card_id, count, sideboard_owner)) 185 | 186 | sideboards.sort(key=lambda x: (x[2], x[0])) 187 | 188 | return cards, heroes, format, sideboards 189 | 190 | 191 | def write_deckstring( 192 | cards: CardIncludeList, 193 | heroes: CardList, 194 | format: FormatType, 195 | sideboards: Optional[SideboardList] = None, 196 | ) -> str: 197 | if sideboards is None: 198 | sideboards = [] 199 | 200 | data = BytesIO() 201 | data.write(b"\0") 202 | _write_varint(data, DECKSTRING_VERSION) 203 | _write_varint(data, int(format)) 204 | 205 | if len(heroes) != 1: 206 | raise ValueError("Unsupported hero count %i" % (len(heroes))) 207 | _write_varint(data, len(heroes)) 208 | for hero in sorted(heroes): 209 | _write_varint(data, hero) 210 | 211 | cards_x1, cards_x2, cards_xn = trisort_cards(cards) 212 | 213 | sort_key = lambda x: x[0] 214 | 215 | for cardlist in sorted(cards_x1, key=sort_key), sorted(cards_x2, key=sort_key): 216 | _write_varint(data, len(cardlist)) 217 | for cardid, _ in cardlist: 218 | _write_varint(data, cardid) 219 | 220 | _write_varint(data, len(cards_xn)) 221 | for cardid, count in sorted(cards_xn, key=sort_key): 222 | _write_varint(data, cardid) 223 | _write_varint(data, count) 224 | 225 | if len(sideboards) > 0: 226 | data.write(b"\1") 227 | 228 | sideboards_x1, sideboards_x2, sideboards_xn = trisort_cards(sideboards) 229 | 230 | sb_sort_key = lambda x: (x[2], x[0]) 231 | 232 | for cardlist in ( 233 | sorted(sideboards_x1, key=sb_sort_key), 234 | sorted(sideboards_x2, key=sb_sort_key) 235 | ): 236 | _write_varint(data, len(cardlist)) 237 | for cardid, _, sideboard_owner in cardlist: 238 | _write_varint(data, cardid) 239 | _write_varint(data, sideboard_owner) 240 | 241 | _write_varint(data, len(sideboards_xn)) 242 | for cardid, count, sideboard_owner in sorted(sideboards_xn, key=sb_sort_key): 243 | _write_varint(data, cardid) 244 | _write_varint(data, count) 245 | _write_varint(data, sideboard_owner) 246 | 247 | else: 248 | data.write(b"\0") 249 | 250 | encoded = base64.b64encode(data.getvalue()) 251 | return encoded.decode("utf-8") 252 | -------------------------------------------------------------------------------- /hearthstone/entities.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast 2 | 3 | from hearthstone.utils import MAESTRA_DISGUISE_DBF_ID, get_original_card_id 4 | 5 | from .enums import CardSet, CardType, GameTag, State, Step, Zone 6 | from .types import GameTagsDict 7 | 8 | 9 | STARTING_HERO_SETS = (CardSet.HERO_SKINS, ) 10 | 11 | 12 | class Entity: 13 | _args: Iterable[str] = () 14 | 15 | def __init__(self, id): 16 | self.id = id 17 | self.game = None 18 | self.tags: GameTagsDict = {} 19 | self.initial_creator = 0 20 | self.initial_zone: Zone = Zone.INVALID 21 | self._initial_controller = 0 22 | 23 | def __repr__(self): 24 | return "%s(id=%r, %s)" % ( 25 | self.__class__.__name__, self.id, 26 | ", ".join("%s=%r" % (k, getattr(self, k)) for k in self._args) 27 | ) 28 | 29 | @property 30 | def controller(self) -> Optional["Player"]: 31 | return self.game.get_player(self.tags.get(GameTag.CONTROLLER, 0)) 32 | 33 | @property 34 | def initial_controller(self): 35 | return self.game.get_player( 36 | self._initial_controller or self.tags.get(GameTag.CONTROLLER, 0) 37 | ) 38 | 39 | @property 40 | def type(self): 41 | return self.tags.get(GameTag.CARDTYPE, CardType.INVALID) 42 | 43 | @property 44 | def zone(self): 45 | return self.tags.get(GameTag.ZONE, Zone.INVALID) 46 | 47 | def _update_tags(self, tags): 48 | for tag, value in tags.items(): 49 | if tag == GameTag.CONTROLLER and not self._initial_controller: 50 | self._initial_controller = self.tags.get(GameTag.CONTROLLER, value) 51 | self.tags.update(tags) 52 | 53 | def reset(self): 54 | pass 55 | 56 | def tag_change(self, tag, value): 57 | self._update_tags({tag: value}) 58 | 59 | 60 | class Game(Entity): 61 | _args = ("players", ) 62 | can_be_in_deck = False 63 | 64 | def __init__(self, id): 65 | super(Game, self).__init__(id) 66 | self.players: List[Player] = [] 67 | self._entities: Dict[int, Entity] = {} 68 | self.initial_entities: List[Entity] = [] 69 | self.initial_state: State = State.INVALID 70 | self.initial_step: Step = Step.INVALID 71 | 72 | @property 73 | def entities(self) -> Iterator[Entity]: 74 | yield from self._entities.values() 75 | 76 | @property 77 | def current_player(self) -> Optional["Player"]: 78 | for player in self.players: 79 | if player.tags.get(GameTag.CURRENT_PLAYER): 80 | return player 81 | return None 82 | 83 | @property 84 | def first_player(self) -> Optional["Player"]: 85 | for player in self.players: 86 | if player.tags.get(GameTag.FIRST_PLAYER): 87 | return player 88 | return None 89 | 90 | @property 91 | def setup_done(self) -> bool: 92 | return self.tags.get(GameTag.NEXT_STEP, 0) > Step.BEGIN_MULLIGAN 93 | 94 | def get_player(self, value: Union[int, str]) -> Optional["Player"]: 95 | for player in self.players: 96 | if value in (player.player_id, player.name): 97 | return player 98 | return None 99 | 100 | def in_zone(self, zone: Zone) -> Iterator[Entity]: 101 | for entity in self.entities: 102 | if entity.zone == zone: 103 | yield entity 104 | 105 | def create(self, tags: GameTagsDict) -> None: 106 | self.tags = dict(tags) 107 | self.initial_state = cast(State, self.tags.get(GameTag.STATE, State.INVALID)) 108 | self.initial_step = cast(Step, self.tags.get(GameTag.STEP, Step.INVALID)) 109 | self.register_entity(self) 110 | 111 | def register_entity(self, entity: Entity) -> None: 112 | entity.game = self 113 | self._entities[entity.id] = entity 114 | entity.initial_zone = entity.zone 115 | 116 | if isinstance(entity, Player): 117 | self.players.append(entity) 118 | elif not self.setup_done: 119 | self.initial_entities.append(entity) 120 | 121 | # Infer player class and card from "Maestra of the Masquerade" revealing herself 122 | if ( 123 | entity.type == CardType.HERO and 124 | entity.tags.get(GameTag.CREATOR_DBID) == MAESTRA_DISGUISE_DBF_ID 125 | ): 126 | player = entity.controller 127 | if player is not None: 128 | # The player was playing Maestra, which created a fake hero at the start of 129 | # the game. After playing a Rogue card, the real hero is revealed, which 130 | # creates a new hero entity. To ensure that player.starting_hero returns the 131 | # "correct" Rogue hero, we overwrite the initial_hero_entity_id with the new 132 | # one. 133 | player.initial_hero_entity_id = entity.id 134 | 135 | # At this point we know that Maestra must be in the starting deck of the 136 | # player, because otherwise the reveal would not happen. Manually add it to 137 | # the list of starting cards 138 | player._known_starting_card_ids.add("SW_050") 139 | 140 | # Infer Tourists when they reveal themselves 141 | if ( 142 | entity.tags.get(GameTag.ZONE) == Zone.REMOVEDFROMGAME and 143 | entity.tags.get(GameTag.TOURIST, 0) > 0 144 | ): 145 | # This might be the fake Tourist that the game pops up to explain why a card was 146 | # present in the player's deck. Double-check that the card was created by the 147 | # Tourist VFX enchantment. 148 | creator_id = entity.tags.get(GameTag.CREATOR) 149 | creator = self.find_entity_by_id(creator_id) if creator_id else None 150 | creator_is_vfx = getattr(creator, "card_id", None) == "VAC_422e" 151 | player = entity.controller 152 | tourist_card_id = getattr(entity, "card_id", None) 153 | if creator_is_vfx and player is not None and tourist_card_id is not None: 154 | player._known_starting_card_ids.add(tourist_card_id) 155 | 156 | def reset(self) -> None: 157 | for entity in self.entities: 158 | if entity is self: 159 | continue 160 | entity.reset() 161 | 162 | def find_entity_by_id(self, id: int) -> Optional[Entity]: 163 | # int() for LazyPlayer mainly... 164 | id = int(id) 165 | return self._entities.get(id) 166 | 167 | 168 | class Player(Entity): 169 | _args = ("name", ) 170 | UNKNOWN_HUMAN_PLAYER = "UNKNOWN HUMAN PLAYER" 171 | can_be_in_deck = False 172 | 173 | def __init__(self, id, player_id, hi, lo, name=None): 174 | super(Player, self).__init__(id) 175 | self.player_id = player_id 176 | self.account_hi = hi 177 | self.account_lo = lo 178 | self.name = name 179 | self.initial_hero_entity_id = 0 180 | self._known_starting_card_ids = set() 181 | 182 | def __str__(self) -> str: 183 | return self.name or "" 184 | 185 | @property 186 | def names(self) -> Tuple[str, str]: 187 | """ 188 | Returns the player's name and real name. 189 | Returns two empty strings if the player is unknown. 190 | AI real name is always an empty string. 191 | """ 192 | if self.name == self.UNKNOWN_HUMAN_PLAYER: 193 | return "", "" 194 | 195 | if not self.is_ai and " " in self.name: 196 | return "", self.name 197 | 198 | return self.name, "" 199 | 200 | @property 201 | def initial_deck(self) -> Iterator["Card"]: 202 | for entity in self.game.initial_entities: 203 | # Exclude entities that aren't initially owned by the player 204 | if entity.initial_controller != self: 205 | continue 206 | 207 | # Exclude entities that aren't initially in the deck 208 | # We include the graveyard because of Souleater's Scythe, that moves 209 | # into the graveyard before the mulligan. 210 | if entity.initial_zone not in (Zone.DECK, Zone.GRAVEYARD): 211 | continue 212 | 213 | # Exclude entity types that cannot be in the deck 214 | if not entity.can_be_in_deck: 215 | continue 216 | 217 | # Allow CREATOR=1 because of monster hunt decks. 218 | # Everything else is likely a false positive. 219 | if entity.initial_creator > 1: 220 | continue 221 | 222 | yield entity 223 | 224 | @property 225 | def known_starting_deck_list(self) -> List[str]: 226 | """ 227 | Returns a list of card ids that were present in the player's deck at the start of 228 | game (before Mulligan). May contain duplicates if same card is present multiple 229 | times in the deck. This attempts to reverse revealed transforms (e.g. Zerus, Molten 230 | Blade) and well-known transforms (e.g. Spellstones, Unidentified Objects, Worgens) 231 | so that the initial card id is included rather than the final card id. 232 | """ 233 | original_card_ids = [ 234 | get_original_card_id(entity.initial_card_id) 235 | for entity in self.initial_deck if entity.initial_card_id 236 | ] 237 | return original_card_ids + [ 238 | card_id for card_id in self._known_starting_card_ids 239 | if card_id not in original_card_ids 240 | ] 241 | 242 | @property 243 | def entities(self) -> Iterator[Entity]: 244 | for entity in self.game.entities: 245 | if entity.controller == self: 246 | yield entity 247 | 248 | @property 249 | def hero(self) -> Optional["Card"]: 250 | entity_id = self.tags.get(GameTag.HERO_ENTITY, 0) 251 | if entity_id: 252 | return self.game.find_entity_by_id(entity_id) 253 | else: 254 | # Fallback that should never trigger 255 | for entity in self.in_zone(Zone.PLAY): 256 | if entity.type == CardType.HERO: 257 | return cast(Card, entity) 258 | return None 259 | 260 | @property 261 | def heroes(self) -> Iterator["Card"]: 262 | for entity in self.entities: 263 | if entity.type == CardType.HERO: 264 | yield cast(Card, entity) 265 | 266 | @property 267 | def starting_hero(self) -> Optional["Card"]: 268 | if self.initial_hero_entity_id: 269 | return cast(Card, self.game.find_entity_by_id(self.initial_hero_entity_id)) 270 | 271 | # Fallback 272 | heroes = list(self.heroes) 273 | if not heroes: 274 | return None 275 | 276 | return heroes[0] 277 | 278 | @property 279 | def is_ai(self) -> bool: 280 | return self.account_lo == 0 281 | 282 | def in_zone(self, zone) -> Iterator["Entity"]: 283 | for entity in self.entities: 284 | if entity.zone == zone: 285 | yield entity 286 | 287 | 288 | class Card(Entity): 289 | _args = ("card_id", ) 290 | 291 | def __init__(self, id, card_id): 292 | super(Card, self).__init__(id) 293 | self.is_original_entity = True 294 | self.initial_card_id = card_id 295 | self.card_id = card_id 296 | self.revealed = False 297 | 298 | @property 299 | def base_tags(self) -> GameTagsDict: 300 | if not self.card_id: 301 | return {} 302 | 303 | from .cardxml import load 304 | db, _ = load() 305 | return db[self.card_id].tags 306 | 307 | def _get_initial_base_tags(self) -> GameTagsDict: 308 | if not self.initial_card_id: 309 | return {} 310 | 311 | from .cardxml import load 312 | db, _ = load() 313 | return db[self.initial_card_id].tags 314 | 315 | @property 316 | def can_be_in_deck(self) -> bool: 317 | card_type = self.type 318 | if not card_type: 319 | # If we don't know the card type, assume yes 320 | return True 321 | elif card_type == CardType.HERO: 322 | tags = self._get_initial_base_tags() 323 | return ( 324 | tags.get(GameTag.CARD_SET, 0) not in STARTING_HERO_SETS and 325 | bool(tags.get(GameTag.COLLECTIBLE, 0)) 326 | ) 327 | 328 | return CardType(card_type).playable 329 | 330 | def _capture_initial_card_id(self, card_id: str, tags: GameTagsDict) -> None: 331 | if self.initial_card_id: 332 | # If we already know a previous card id, we do not want to change it. 333 | return 334 | 335 | transformed_from_card = tags.get(GameTag.TRANSFORMED_FROM_CARD, 0) 336 | if transformed_from_card: 337 | from .cardxml import load_dbf 338 | db, _ = load_dbf() 339 | card = db.get(transformed_from_card) 340 | if card: 341 | self.initial_card_id = card.card_id 342 | return 343 | 344 | if not self.is_original_entity: 345 | # If we know this card was transformed and we don't have an initial_card_id by 346 | # now, it is too late - any card_id we'd capture now would not reflect initial 347 | # one and be wrong. 348 | return 349 | 350 | self.initial_card_id = card_id 351 | 352 | def _update_tags(self, tags: GameTagsDict) -> None: 353 | super()._update_tags(tags) 354 | if self.is_original_entity and self.initial_creator is None: 355 | creator = tags.get(GameTag.CREATOR, 0) 356 | if creator: 357 | self.initial_creator = creator 358 | 359 | def reveal(self, card_id: str, tags: GameTagsDict) -> None: 360 | self.revealed = True 361 | self.card_id = card_id 362 | 363 | if ( 364 | tags.get(GameTag.CREATOR_DBID, 0) or 365 | tags.get(GameTag.DISPLAYED_CREATOR, 0) or 366 | tags.get(GameTag.TRANSFORMED_FROM_CARD, 0) 367 | ): 368 | # Cards that are revealed with a creator most likely have been transformed. 369 | self.is_original_entity = False 370 | 371 | self._capture_initial_card_id(card_id, tags) 372 | self._update_tags(tags) 373 | 374 | def hide(self) -> None: 375 | self.revealed = False 376 | 377 | def change(self, card_id: str, tags) -> None: 378 | self._capture_initial_card_id(card_id, tags) 379 | self.is_original_entity = False 380 | self.card_id = card_id 381 | self._update_tags(tags) 382 | 383 | def reset(self) -> None: 384 | self.card_id = None 385 | self.revealed = False 386 | -------------------------------------------------------------------------------- /hearthstone/enums.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import IntEnum 3 | 4 | 5 | class GameTag(IntEnum): 6 | """GAME_TAG""" 7 | 8 | TAG_NOT_SET = 0 9 | TAG_SCRIPT_DATA_NUM_1 = 2 10 | TAG_SCRIPT_DATA_NUM_2 = 3 11 | TAG_SCRIPT_DATA_ENT_1 = 4 12 | TAG_SCRIPT_DATA_ENT_2 = 5 13 | MISSION_EVENT = 6 14 | TIMEOUT = 7 15 | TURN_START = 8 16 | TURN_TIMER_SLUSH = 9 17 | PREMIUM = 12 18 | PLAYSTATE = 17 19 | LAST_AFFECTED_BY = 18 20 | STEP = 19 21 | TURN = 20 22 | FATIGUE = 22 23 | CURRENT_PLAYER = 23 24 | FIRST_PLAYER = 24 25 | RESOURCES_USED = 25 26 | RESOURCES = 26 27 | HERO_ENTITY = 27 28 | MAXHANDSIZE = 28 29 | STARTHANDSIZE = 29 30 | PLAYER_ID = 30 31 | TEAM_ID = 31 32 | TRIGGER_VISUAL = 32 33 | RECENTLY_ARRIVED = 33 34 | PROTECTED = 34 35 | PROTECTING = 35 36 | DEFENDING = 36 37 | PROPOSED_DEFENDER = 37 38 | ATTACKING = 38 39 | PROPOSED_ATTACKER = 39 40 | ATTACHED = 40 41 | EXHAUSTED = 43 42 | DAMAGE = 44 43 | HEALTH = 45 44 | ATK = 47 45 | COST = 48 46 | ZONE = 49 47 | CONTROLLER = 50 48 | OWNER = 51 49 | DEFINITION = 52 50 | ENTITY_ID = 53 51 | HISTORY_PROXY = 54 52 | ELITE = 114 53 | MAXRESOURCES = 176 54 | CARD_SET = 183 55 | CARDTEXT = 184 56 | DURABILITY = 187 57 | SILENCED = 188 58 | WINDFURY = 189 59 | TAUNT = 190 60 | STEALTH = 191 61 | SPELLPOWER = 192 62 | DIVINE_SHIELD = 194 63 | CHARGE = 197 64 | NEXT_STEP = 198 65 | CLASS = 199 66 | CARDRACE = 200 67 | FACTION = 201 68 | CARDTYPE = 202 69 | RARITY = 203 70 | STATE = 204 71 | SUMMONED = 205 72 | FREEZE = 208 73 | ENRAGED = 212 74 | OVERLOAD = 215 75 | LOYALTY = 216 76 | DEATHRATTLE = 217 77 | BATTLECRY = 218 78 | SECRET = 219 79 | COMBO = 220 80 | CANT_HEAL = 221 81 | CANT_DAMAGE = 222 82 | CANT_SET_ASIDE = 223 83 | CANT_REMOVE_FROM_GAME = 224 84 | CANT_READY = 225 85 | CANT_ATTACK = 227 86 | CANT_DISCARD = 230 87 | CANT_PLAY = 231 88 | CANT_DRAW = 232 89 | CANT_BE_HEALED = 239 90 | IMMUNE = 240 91 | CANT_BE_SET_ASIDE = 241 92 | CANT_BE_REMOVED_FROM_GAME = 242 93 | CANT_BE_READIED = 243 94 | CANT_BE_ATTACKED = 245 95 | CANT_BE_TARGETED = 246 96 | CANT_BE_DESTROYED = 247 97 | CANT_BE_SUMMONING_SICK = 253 98 | FROZEN = 260 99 | JUST_PLAYED = 261 100 | LINKED_ENTITY = 262 101 | ZONE_POSITION = 263 102 | CANT_BE_FROZEN = 264 103 | COMBO_ACTIVE = 266 104 | CARD_TARGET = 267 105 | NUM_CARDS_PLAYED_THIS_TURN = 269 106 | CANT_BE_TARGETED_BY_OPPONENTS = 270 107 | NUM_TURNS_IN_PLAY = 271 108 | NUM_TURNS_LEFT = 272 109 | NUM_TURNS_IN_HAND = 273 110 | CURRENT_SPELLPOWER = 291 111 | ARMOR = 292 112 | MORPH = 293 113 | IS_MORPHED = 294 114 | TEMP_RESOURCES = 295 115 | OVERLOAD_OWED = 296 116 | NUM_ATTACKS_THIS_TURN = 297 117 | NEXT_ALLY_BUFF = 302 118 | MAGNET = 303 119 | FIRST_CARD_PLAYED_THIS_TURN = 304 120 | MULLIGAN_STATE = 305 121 | TAUNT_READY = 306 122 | STEALTH_READY = 307 123 | CHARGE_READY = 308 124 | CANT_BE_TARGETED_BY_SPELLS = 311 125 | SHOULDEXITCOMBAT = 312 126 | CREATOR = 313 127 | CANT_BE_SILENCED = 314 128 | PARENT_CARD = 316 129 | NUM_MINIONS_PLAYED_THIS_TURN = 317 130 | PREDAMAGE = 318 131 | COLLECTIBLE = 321 132 | HEALING_DOES_DAMAGE = 326 133 | DATABASE_ID = 327 134 | ENCHANTMENT_BIRTH_VISUAL = 330 135 | ENCHANTMENT_IDLE_VISUAL = 331 136 | CANT_BE_TARGETED_BY_HERO_POWERS = 332 137 | HEALTH_MINIMUM = 337 138 | TAG_ONE_TURN_EFFECT = 338 139 | SILENCE = 339 140 | COUNTER = 340 141 | ZONES_REVEALED = 348 142 | ADJACENT_BUFF = 350 143 | FORCED_PLAY = 352 144 | LOW_HEALTH_THRESHOLD = 353 145 | SPELLPOWER_DOUBLE = 356 146 | SPELL_HEALING_DOUBLE = 357 147 | NUM_OPTIONS_PLAYED_THIS_TURN = 358 148 | TO_BE_DESTROYED = 360 149 | AURA = 362 150 | POISONOUS = 363 151 | HERO_POWER_DOUBLE = 366 152 | AI_MUST_PLAY = 367 153 | NUM_MINIONS_PLAYER_KILLED_THIS_TURN = 368 154 | NUM_MINIONS_KILLED_THIS_TURN = 369 155 | AFFECTED_BY_SPELL_POWER = 370 156 | EXTRA_MINION_DEATHRATTLES_BASE = 371 157 | START_WITH_1_HEALTH = 372 158 | IMMUNE_WHILE_ATTACKING = 373 159 | MULTIPLY_HERO_DAMAGE = 374 160 | MULTIPLY_BUFF_VALUE = 375 161 | CUSTOM_KEYWORD_EFFECT = 376 162 | CANT_BE_TARGETED_BY_BATTLECRIES = 379 163 | HERO_POWER = 380 164 | DEATHRATTLE_RETURN_ZONE = 382 165 | STEADY_SHOT_CAN_TARGET = 383 166 | DISPLAYED_CREATOR = 385 167 | POWERED_UP = 386 168 | SPARE_PART = 388 169 | FORGETFUL = 389 170 | CAN_SUMMON_MAXPLUSONE_MINION = 390 171 | OBFUSCATED = 391 172 | BURNING = 392 173 | OVERLOAD_LOCKED = 393 174 | NUM_TIMES_HERO_POWER_USED_THIS_GAME = 394 175 | CURRENT_HEROPOWER_DAMAGE_BONUS = 395 176 | HEROPOWER_DAMAGE = 396 177 | NUM_FRIENDLY_MINIONS_THAT_DIED_THIS_TURN = 398 178 | NUM_CARDS_DRAWN_THIS_TURN = 399 179 | AI_ONE_SHOT_KILL = 400 180 | EVIL_GLOW = 401 181 | HIDE_STATS = 402 182 | INSPIRE = 403 183 | RECEIVES_DOUBLE_SPELLDAMAGE_BONUS = 404 184 | HEROPOWER_ADDITIONAL_ACTIVATIONS = 405 185 | HEROPOWER_ACTIVATIONS_THIS_TURN = 406 186 | REVEALED = 410 187 | EXTRA_BATTLECRIES_BASE = 411 188 | NUM_FRIENDLY_MINIONS_THAT_DIED_THIS_GAME = 412 189 | CANNOT_ATTACK_HEROES = 413 190 | LOCK_AND_LOAD = 414 191 | DISCOVER = 415 192 | SHADOWFORM = 416 193 | NUM_FRIENDLY_MINIONS_THAT_ATTACKED_THIS_TURN = 417 194 | NUM_RESOURCES_SPENT_THIS_GAME = 418 195 | CHOOSE_BOTH = 419 196 | ELECTRIC_CHARGE_LEVEL = 420 197 | HEAVILY_ARMORED = 421 198 | DONT_SHOW_IMMUNE = 422 199 | PREHEALING = 425 200 | APPEAR_FUNCTIONALLY_DEAD = 426 201 | OVERLOAD_THIS_GAME = 427 202 | SPELLS_COST_HEALTH = 431 203 | HISTORY_PROXY_NO_BIG_CARD = 432 204 | IGNORE_TAUNT = 433 205 | TRANSFORMED_FROM_CARD = 435 206 | CTHUN = 436 207 | CAST_RANDOM_SPELLS = 437 208 | SHIFTING = 438 209 | JADE_GOLEM = 441 210 | EMBRACE_THE_SHADOW = 442 211 | CHOOSE_ONE = 443 212 | EXTRA_ATTACKS_THIS_TURN = 444 213 | SEEN_CTHUN = 445 214 | MINION_TYPE_REFERENCE = 447 215 | UNTOUCHABLE = 448 216 | RED_MANA_GEM = 449 217 | SCORE_LABELID_1 = 450 218 | SCORE_VALUE_1 = 451 219 | SCORE_LABELID_2 = 452 220 | SCORE_LABELID_3 = 454 221 | SCORE_VALUE_2 = 453 222 | SCORE_VALUE_3 = 455 223 | CANT_BE_FATIGUED = 456 224 | AUTO_ATTACK = 457 225 | ARMS_DEALING = 458 226 | QUEST = 462 227 | TAG_LAST_KNOWN_COST_IN_HAND = 466 228 | DEFINING_ENCHANTMENT = 469 229 | FINISH_ATTACK_SPELL_ON_DAMAGE = 470 230 | MODULAR_ENTITY_PART_1 = 471 231 | MODULAR_ENTITY_PART_2 = 472 232 | MODIFY_DEFINITION_ATTACK = 473 233 | MODIFY_DEFINITION_HEALTH = 474 234 | MODIFY_DEFINITION_COST = 475 235 | MULTIPLE_CLASSES = 476 236 | ALL_TARGETS_RANDOM = 477 237 | GRIMY_GOONS = 482 238 | JADE_LOTUS = 483 239 | KABAL = 484 240 | ADDITIONAL_PLAY_REQS_1 = 515 241 | ADDITIONAL_PLAY_REQS_2 = 516 242 | ELEMENTAL_POWERED_UP = 532 243 | QUEST_PROGRESS = 534 244 | QUEST_PROGRESS_TOTAL = 535 245 | QUEST_CONTRIBUTOR = 541 246 | ADAPT = 546 247 | IS_CURRENT_TURN_AN_EXTRA_TURN = 547 248 | EXTRA_TURNS_TAKEN_THIS_GAME = 548 249 | TREASURE = 557 250 | TREASURE_DEFINTIONAL_ATTACK = 558 251 | TREASURE_DEFINTIONAL_COST = 559 252 | TREASURE_DEFINTIONAL_HEALTH = 560 253 | ACTS_LIKE_A_SPELL = 561 254 | SHIFTING_MINION = 549 255 | SHIFTING_WEAPON = 550 256 | DEATH_KNIGHT = 554 257 | BOSS = 556 258 | STAMPEDE = 564 259 | EMPOWERED_TREASURE = 646 260 | ONE_SIDED_GHOSTLY = 648 261 | CURRENT_NEGATIVE_SPELLPOWER = 651 262 | DONT_PICK_FROM_SUBSETS = 676 263 | IS_VAMPIRE = 680 264 | CORRUPTED = 681 265 | HIDE_HEALTH = 682 266 | HIDE_ATTACK = 683 267 | HIDE_COST = 684 268 | LIFESTEAL = 685 269 | OVERRIDE_EMOTE_0 = 740 270 | OVERRIDE_EMOTE_1 = 741 271 | OVERRIDE_EMOTE_2 = 742 272 | OVERRIDE_EMOTE_3 = 743 273 | OVERRIDE_EMOTE_4 = 744 274 | OVERRIDE_EMOTE_5 = 745 275 | SCORE_FOOTERID = 751 276 | RECRUIT = 763 277 | LOOT_CARD_1 = 764 278 | LOOT_CARD_2 = 765 279 | LOOT_CARD_3 = 766 280 | HERO_POWER_DISABLED = 777 281 | VALEERASHADOW = 779 282 | OVERRIDECARDNAME = 781 283 | OVERRIDECARDTEXTBUILDER = 782 284 | DUNGEON_PASSIVE_BUFF = 783 285 | GHOSTLY = 785 286 | DISGUISED_TWIN = 788 287 | SECRET_DEATHRATTLE = 789 288 | RUSH = 791 289 | REVEAL_CHOICES = 792 290 | HERO_DECK_ID = 793 291 | HIDDEN_CHOICE = 813 292 | ZOMBEAST = 823 293 | HERO_EMOTE_SILENCED = 832 294 | MINION_IN_HAND_BUFF = 845 295 | ECHO = 846 296 | MAGNETIC = 849 297 | IGNORE_HIDE_STATS_FOR_BIG_CARD = 857 298 | REAL_TIME_TRANSFORM = 859 299 | WAIT_FOR_PLAYER_RECONNECT_PERIOD = 860 300 | ETHEREAL = 880 301 | EXTRA_DEATHRATTLES_BASE = 882 302 | PHASED_RESTART = 888 303 | HEALTH_DISPLAY = 917 304 | ENABLE_HEALTH_DISPLAY = 920 305 | VOODOO_LINK = 921 306 | OVERKILL = 923 307 | PROPHECY = 924 308 | ATTACKABLE_BY_RUSH = 930 309 | SHIFTING_SPELL = 936 310 | USE_ALTERNATE_CARD_TEXT = 955 311 | SUPPRESS_DEATH_SOUND = 959 312 | ECHOING_OOZE_SPELL = 963 313 | COLLECTIONMANAGER_FILTER_MANA_EVEN = 956 314 | COLLECTIONMANAGER_FILTER_MANA_ODD = 957 315 | AMOUNT_HEALED_THIS_GAME = 958 316 | ZOMBEAST_DEBUG_CURRENT_BEAST_DATABASE_ID = 964 317 | ZOMBEAST_DEBUG_CURRENT_ITERATION = 965 318 | ZOMBEAST_DEBUG_MAX_ITERATIONS = 966 319 | START_OF_GAME_KEYWORD = 968 320 | ENCHANTMENT_INVISIBLE = 976 321 | PUZZLE = 979 322 | PUZZLE_PROGRESS = 980 323 | PUZZLE_PROGRESS_TOTAL = 981 324 | PUZZLE_TYPE = 982 325 | PUZZLE_COMPLETED = 984 326 | CONCEDE_BUTTON_ALTERNATIVE_TEXT = 985 327 | HIDE_RESTART_BUTTON = 990 328 | WILD = 991 329 | HALL_OF_FAME = 992 330 | MARK_OF_EVIL = 994 331 | DECK_RULE_MOD_DECK_SIZE = 997 332 | FAST_BATTLECRY = 998 333 | END_TURN_BUTTON_ALTERNATIVE_APPEARANCE = 1000 334 | WAND = 1015 335 | TREAT_AS_PLAYED_HERO_CARD = 1016 336 | LITERALLY_UNPLAYABLE = 1020 337 | NUM_HERO_POWER_DAMAGE_THIS_GAME = 1025 338 | PUZZLE_NAME = 1026 339 | TURN_INDICATOR_ALTERNATIVE_APPEARANCE = 1027 340 | PREVIOUS_PUZZLE_COMPLETED = 1042 341 | GLORIOUSGLOOP = 1044 342 | HEALTH_DISPLAY_COLOR = 1046 343 | HEALTH_DISPLAY_NEGATIVE = 1047 344 | WHIZBANG_DECK_ID = 1048 345 | HIDE_OUT_OF_CARDS_WARNING = 1050 346 | GEARS = 1052 347 | LUNAHIGHLIGHTHINT = 1054 348 | SUPPRESS_JOBS_DONE_VO = 1055 349 | SHRINE = 1057 350 | ALL_HEALING_DOUBLE = 1058 351 | BLOCK_ALL_INPUT = 1071 352 | PUZZLE_MODE = 1073 353 | CARD_DOES_NOTHING = 1075 354 | CASTS_WHEN_DRAWN = 1077 355 | DISPLAY_CARD_ON_MOUSEOVER = 1078 356 | DECK_POWER_UP = 1080 357 | SIDEKICK = 1081 358 | SIDEKICK_HERO_POWER = 1082 359 | REBORN = 1085 360 | SQUELCH_NON_GAME_TRIGGERS_AND_MODIFIERS = 1087 361 | QUEST_REWARD_DATABASE_ID = 1089 362 | DORMANT_VISUAL = 1090 363 | CUSTOMTEXT1 = 1093 364 | CUSTOMTEXT2 = 1094 365 | CUSTOMTEXT3 = 1095 366 | FLOOPY = 1097 367 | PLAYER_BASE_SHRINE_DECK_ID = 1099 368 | HIDE_WATERMARK = 1107 369 | EXTRA_MINION_BATTLECRIES_BASE = 1112 370 | RUN_PROGRESS = 1113 371 | NON_KEYWORD_ECHO = 1114 372 | PLAYER_TAG_THRESHOLD_TAG_ID = 1115 373 | PLAYER_TAG_THRESHOLD_VALUE = 1116 374 | HEALING_DOES_DAMAGE_HINT = 1117 375 | AFFECTED_BY_HEALING_DOES_DAMAGE = 1118 376 | DECK_LIST_SORT_ORDER = 1125 377 | EXTRA_BATTLECRIES_ADDITIONAL = 1126 378 | EXTRA_DEATHRATTLES_ADDITIONAL = 1131 379 | ALTERNATE_MOUSE_OVER_CARD = 1132 380 | ENCHANTMENT_BANNER_TEXT = 1135 381 | MOUSE_OVER_CARD_APPEARANCE = 1142 382 | IS_ADVENTURE_SCENARIO = 1172 383 | DEPRECATED_TWINSPELL_COPY = 1186 384 | PROXY_GALAKROND = 1190 385 | SIDE_QUEST = 1192 386 | TWINSPELL = 1193 387 | GALAKROND_IN_PLAY = 1194 388 | COIN_MANA_GEM = 1199 389 | MEGA_WINDFURY = 1207 390 | ELUSIVE = 1211 391 | EMPOWER = 1263 392 | EMPOWER_PRIEST = 1264 393 | EMPOWER_ROGUE = 1265 394 | EMPOWER_SHAMAN = 1266 395 | EMPOWER_WARLOCK = 1267 396 | EMPOWER_WARRIOR = 1268 397 | TWINSPELLPENDING = 1269 398 | DRUSTVAR_HORROR_DEBUG_CURRENT_SPELL_DATABASE_ID = 1280 399 | DRUSTVAR_HORROR_DEBUG_CURRENT_ITERATION = 1281 400 | HEROIC_HERO_POWER = 1282 401 | DRUSTVAR_HORROR_DEBUG_MAX_ITERATIONS = 1283 402 | CREATOR_DBID = 1284 403 | FATIGUE_REFERENCE = 1290 404 | HERO_FLYING = 1293 405 | UI_BUFF_HEALTH_UP = 1294 406 | UI_BUFF_SET_COST_ZERO = 1295 407 | UI_BUFF_COST_DOWN = 1296 408 | UI_BUFF_ATK_UP = 1297 409 | UI_BUFF_COST_UP = 1298 410 | DEBUG_DISPLAY_TAG_BOTTOM_RIGHT = 1313 411 | DEBUG_DISPLAY_TAG_TOP_RIGHT = 1314 412 | SMART_DISCOVER_DEBUG_ENTITY_1 = 1318 413 | SMART_DISCOVER_DEBUG_ENTITY_2 = 1319 414 | SMART_DISCOVER_DEBUG_ENTITY_3 = 1320 415 | SMART_DISCOVER_DEBUG_TEST_COMPLETE = 1324 416 | SMART_DISCOVER_DEBUG_PASSIVE_EVAL_RESULT_1 = 1328 417 | SMART_DISCOVER_DEBUG_PASSIVE_EVAL_RESULT_2 = 1329 418 | SMART_DISCOVER_DEBUG_PASSIVE_EVAL_RESULT_3 = 1330 419 | COPIED_BY_KHADGAR = 1326 420 | OUTCAST = 1333 421 | ALTERNATE_CHAPTER_VO = 1334 422 | AI_MAKES_DECISIONS_FOR_PLAYER = 1335 423 | HAS_BEEN_REBORN = 1336 424 | USE_DISCOVER_VISUALS = 1342 425 | DOUBLE_FATIGUE_DAMAGE = 1346 426 | BOARD_VISUAL_STATE = 1347 427 | BACON_DUMMY_PLAYER = 1349 428 | SQUELCH_LIFETIME_EFFECTS = 1350 429 | ALLOW_MOVE_MINION = 1356 430 | TAG_TB_RANDOM_DECK_TIME_ID = 1358 431 | NEXT_OPPONENT_PLAYER_ID = 1360 432 | MAIN_GALAKROND = 1361 433 | GOOD_OL_GENERIC_FRIENDLY_DRAGON_DISCOVER_VISUALS = 1364 434 | GALAKROND_HERO_CARD = 1365 435 | INVOKE_COUNTER = 1366 436 | PLAYER_LEADERBOARD_PLACE = 1373 437 | PLAYER_TECH_LEVEL = 1377 438 | BACON_HERO_POWER_ACTIVATED = 1398 439 | USE_FAST_ACTOR_TRANSITION_ANIMATIONS = 1402 440 | STUDY = 1414 441 | BACON_ODD_PLAYER_OUT = 1415 442 | BACON_IS_KEL_THUZAD = 1423 443 | HIGHLIGHT_ATTACKING_MINION_DURING_COMBAT = 1424 444 | SPELLBURST = 1427 445 | BACON_TRIPLE_UPGRADE_MINION_ID = 1429 446 | RULEBOOK = 1430 447 | FX_DATANUM_1 = 1436 448 | BACON_ACTION_CARD = 1437 449 | GAME_MODE_BUTTON_SLOT = 1438 450 | TECH_LEVEL = 1440 451 | TECH_LEVEL_MANA_GEM = 1442 452 | UI_BUFF_DURABILITY_UP = 1443 453 | PLAYER_TRIPLES = 1447 454 | DISABLE_TURN_INDICATORS = 1448 455 | COLLECTION_RELATED_CARD_DATABASE_ID = 1452 456 | IS_BACON_POOL_MINION = 1456 457 | SUPPRESS_ALL_SUMMON_VO = 1458 458 | BACON_TRIPLE_CANDIDATE = 1460 459 | BATTLEGROUNDS_PREMIUM_EMOTES = 1463 460 | MOVE_MINION_HOVER_TARGET_SLOT = 1464 461 | BACON_COIN_ON_ENEMY_MINIONS = 1467 462 | BACON_TRIPLED_BASE_MINION_ID = 1471 463 | ALWAYS_USE_FAST_ACTOR_TRIGGERS = 1473 464 | BACON_HERO_CAN_BE_DRAFTED = 1491 465 | TRANSIENT_ENTITY = 1493 466 | BACON_MAX_PLAYER_TECH_LEVEL = 1494 467 | DISABLE_NONHERO_GOLDEN_ANIMATIONS = 1514 468 | WATERMARK_OVERRIDE_CARD_SET = 1517 469 | DORMANT = 1518 470 | DORMANT_AWAKEN_CONDITION_ENCHANT = 1519 471 | SUPPRESS_SUMMON_VO_FOR_PLAYER = 1521 472 | CORRUPT = 1524 473 | ALLOW_GAME_SPEEDUP = 1526 474 | POISONOUS_INSTANT = 1528 475 | FORCE_NO_CUSTOM_SPELLS = 1529 476 | START_OF_COMBAT = 1531 477 | CORRUPTED_CARD = 1551 478 | BACON_HERO_EARLY_ACCESS = 1554 479 | SPAWN_TIME_COUNT = 1556 480 | SKIP_MULLIGAN = 1561 481 | COPIED_FROM_ENTITY_ID = 1565 482 | BACON_SELL_VALUE = 1587 483 | BACON_VERDANTSPHERES = 1598 484 | OPPONENT_SIDE_GHOSTLY = 1609 485 | FORCE_NO_CUSTOM_LIFETIME_SPELLS = 1613 486 | FORCE_NO_CUSTOM_SUMMON_SPELLS = 1614 487 | FORCE_NO_CUSTOM_KEYWORD_SPELLS = 1615 488 | USE_LEADERBOARD_AS_SPAWN_ORIGIN = 1628 489 | BACON_MUKLA_BANANA_SPAWN_COUNT = 1629 490 | REPLACEMENT_ENTITY = 1632 491 | SPELL_SCHOOL = 1635 492 | FRENZY = 1637 493 | COIN_MANA_GEM_FOR_CHOICE_CARDS = 1643 494 | METAMORPHOSIS = 1644 495 | HERO_POWER_ENTITY = 1646 496 | BACON_PLAYER_RESULTS_HERO_OVERRIDE = 1649 497 | DISCOVER_STUDIES_VISUAL = 1650 498 | LETTUCE_CONTROLLER = 1653 499 | LETTUCE_ABILITY_OWNER = 1654 500 | LETTUCE_SELECTED_TARGET = 1657 501 | LETTUCE_SELECTED_SUBCARD_INDEX = 1661 502 | LETTUCE_MERCENARY = 1665 503 | LETTUCE_ROLE = 1666 504 | LETTUCE_IS_COMBAT_ACTION_TAKEN = 1668 505 | LETTUCE_COOLDOWN_CONFIG = 1669 506 | LETTUCE_CURRENT_COOLDOWN = 1670 507 | LETTUCE_PASSIVE_ABILITY = 1671 508 | LIFESTEAL_DAMAGES_OPPOSING_HERO = 1675 509 | LETTUCE_ABILITY_SUMMONED_MINION = 1676 510 | SPELLS_CAST_TWICE = 1681 511 | CHOICE_NAME_DISPLAY_TYPE = 1687 512 | CHOICE_ACTOR_TYPE = 1692 513 | FORCE_GREEN_GLOW_ACTIVE = 1693 514 | SOURCE_OVERRIDE_FOR_MODIFIER_TEXT = 1694 515 | LETTUCE_ABILITY_TILE_VISUAL_SELF_ONLY = 1697 516 | LETTUCE_ABILITY_TILE_VISUAL_ALL_VISIBLE = 1698 517 | ACTION_STEP_TYPE = 1700 518 | FAKE_ZONE = 1702 519 | FAKE_ZONE_POSITION = 1703 520 | LETTUCE_MAX_IN_PLAY_MERCENARIES = 1704 521 | LETTUCE_MERCENARIES_TO_NOMINATE = 1705 522 | LETTUCE_COOLDOWN_WHILE_BENCHED = 1708 523 | LETTUCE_COMBAT_FROM_HIGH_TO_LOW = 1712 524 | PENDING_TRANSFORM_TO_CARD = 1716 525 | TRANSFORMED_FROM_CARD_VISUAL_TYPE = 1719 526 | TRADEABLE = 1720 527 | TOOL = 1722 528 | QUESTLINE = 1725 529 | LETTUCE_MERCENARY_RESERVE = 1731 530 | LETTUCE_SKIP_MERCENARY_RESERVE = 1732 531 | PLAYER_ID_LOOKUP = 1740 532 | DECK_ACTION_COST = 1743 533 | BACON_AVALANCHE = 1744 534 | SIGIL = 1749 535 | LETTUCE_DISABLE_AUTO_SELECT_NEXT_MERC = 1753 536 | PLAYED_CTHUN_EYE = 1764 537 | PLAYED_CTHUN_BODY = 1765 538 | PLAYED_CTHUN_MAW = 1766 539 | PLAYED_CTHUN_HEART = 1767 540 | PROXY_CTHUN_SHATTERED = 1768 541 | PROGRESSBAR_TOTAL = 1769 542 | PROGRESSBAR_PROGRESS = 1770 543 | PROGRESSBAR_CARDID = 1771 544 | PROGRESSBAR_SHOW = 1772 545 | PROGRESSBAR_TEXT = 1773 546 | LIFESTEAL_DOES_DAMAGE_HINT = 1774 547 | DARKMOON_TICKET = 1776 548 | NUM_SPELLS_PLAYED_THIS_GAME = 1780 549 | BACON_COMEONECOMEALL = 1789 550 | LETTUCE_ABILITY_USED_LAST_TURN = 1807 551 | LETTUCE_NODE_TYPE = 1808 552 | SHOW_DISCOVER_FROM_DECK = 1816 553 | MINI_SET = 1824 554 | ARMOR_GAINED_THIS_GAME = 1828 555 | CANT_TRIGGER_DEATHRATTLE = 1831 556 | BACON_BLOODGEMBUFFATKVALUE = 1844 557 | CANT_MOVE_MINION = 1848 558 | LETTUCE_MERCENARY_EXPERIENCE = 1852 559 | LETTUCE_IS_EQUPIMENT = 1855 560 | LETTUCE_EQUIPMENT_ID = 1856 561 | DARKMOON_FAIRE_PRIZES_ACTIVE = 1895 562 | IGNORE_DECK_RULESET = 1896 563 | HONORABLE_KILL = 1920 564 | HAS_DIAMOND_QUALITY = 1932 565 | CURRENT_SPELLPOWER_ARCANE = 1936 566 | CURRENT_SPELLPOWER_FIRE = 1937 567 | CURRENT_SPELLPOWER_FROST = 1938 568 | CURRENT_SPELLPOWER_NATURE = 1939 569 | CURRENT_SPELLPOWER_HOLY = 1940 570 | CURRENT_SPELLPOWER_SHADOW = 1941 571 | CURRENT_SPELLPOWER_FEL = 1942 572 | CURRENT_SPELLPOWER_PHYSICAL = 1943 573 | NON_KEYWORD_POISONOUS = 1944 574 | SPELLPOWER_ARCANE = 1945 575 | SPELLPOWER_FIRE = 1946 576 | SPELLPOWER_FROST = 1947 577 | SPELLPOWER_NATURE = 1948 578 | SPELLPOWER_HOLY = 1949 579 | SPELLPOWER_SHADOW = 1950 580 | SPELLPOWER_FEL = 1951 581 | SPELLPOWER_PHYSICAL = 1952 582 | ENRAGE_TOOLTIP = 1954 583 | IMP = 1965 584 | BACON_BLOOD_GEM_TOOLTIP = 1966 585 | LETTUCE_HAS_MANUALLY_SELECTED_ABILITY = 1967 586 | LETTUCE_KEEP_LAST_STANDING_MINION_ACTOR = 1976 587 | GOLDSPARKLES_HINT = 1984 588 | LETTUCE_USE_DETERMINISTIC_TEAM_ABILITY_QUEUING = 1990 589 | LETTUCE_SELECTED_ABILITY_QUEUE_ORDER = 1991 590 | QUESTLINE_FINAL_REWARD_DATABASE_ID = 1992 591 | QUESTLINE_PART = 1993 592 | QUESTLINE_REQUIREMENT_MET_1 = 1994 593 | QUESTLINE_REQUIREMENT_MET_2 = 1995 594 | QUESTLINE_REQUIREMENT_MET_3 = 1996 595 | DONT_SHOW_IN_HISTORY = 2015 596 | MAX_SLOTS_PER_PLAYER_OVERRIDE = 2017 597 | FAKE_CONTROLLER = 2032 598 | BACON_SKIN = 2038 599 | BACON_SKIN_PARENT_ID = 2039 600 | GAME_SEED = 2042 601 | IS_USING_TRADE_OPTION = 2045 602 | BACON_BOB_SKIN = 2049 603 | COIN_CARD = 2088 604 | BACON_COMBAT_DAMAGE_CAP = 2089 605 | BACON_REFRESH_TOOLTIP = 2104 606 | TARGETING_ARROW_TYPE = 2108 607 | LETTUCE_CURRENT_BOUNTY_ID = 2120 608 | LETTUCE_OVERTIME = 2123 609 | AVENGE = 2129 610 | BACON_COMPANION_ID = 2130 611 | SPELL_RESISTANCE_ARCANE = 2138 612 | SPELL_RESISTANCE_FIRE = 2139 613 | SPELL_RESISTANCE_FROST = 2140 614 | SPELL_RESISTANCE_NATURE = 2141 615 | SPELL_RESISTANCE_HOLY = 2142 616 | SPELL_RESISTANCE_SHADOW = 2143 617 | SPELL_RESISTANCE_FEL = 2144 618 | SPELL_WEAKNESS_ARCANE = 2145 619 | SPELL_WEAKNESS_FIRE = 2146 620 | SPELL_WEAKNESS_FROST = 2147 621 | SPELL_WEAKNESS_NATURE = 2148 622 | SPELL_WEAKNESS_HOLY = 2149 623 | SPELL_WEAKNESS_SHADOW = 2150 624 | SPELL_WEAKNESS_FEL = 2151 625 | BACON_BUDDY = 2154 626 | BACON_STARSTOBOUNCEOFF = 2155 627 | LETTUCE_KEYWORD_ATTACK = 2159 628 | LETTUCE_KEYWORD_SPELL_COMBO = 2160 629 | LETTUCE_BOUNTY_BOSS = 2168 630 | LETTUCE_IS_TREASURE_CARD = 2170 631 | LETTUCE_SPELLDAMAGEARCANE = 2171 632 | LETTUCE_SPELLDAMAGEFEL = 2172 633 | LETTUCE_SPELLDAMAGEFIRE = 2173 634 | LETTUCE_SPELLDAMAGEFROST = 2174 635 | LETTUCE_SPELLDAMAGEHOLY = 2175 636 | LETTUCE_SPELLDAMAGENATURE = 2176 637 | LETTUCE_SPELLDAMAGESHADOW = 2177 638 | ROOTED = 2179 639 | VULNERABLE = 2180 640 | DEATHBLOW = 2185 641 | CORPSES = 2186 642 | MAXRESOURCES_BLOOD = 2188 643 | MAXRESOURCES_FROST = 2189 644 | MAXRESOURCES_UNHOLY = 2190 645 | MAXRESOURCES_DEATH = 2191 646 | RESOURCES_BLOOD = 2192 647 | RESOURCES_FROST = 2193 648 | RESOURCES_UNHOLY = 2194 649 | RESOURCES_DEATH = 2195 650 | COST_BLOOD = 2196 651 | COST_FROST = 2197 652 | COST_UNHOLY = 2198 653 | COST_DEATH = 2199 654 | HAS_BLOOD_PLAGUE = 2211 655 | HAS_FROST_PLAGUE = 2212 656 | HAS_UNHOLY_PLAGUE = 2213 657 | LETTUCE_BLEED = 2214 658 | LETTUCE_KEYWORD_CRITICAL_DAMAGE = 2219 659 | LETTUCE_KEYWORD_ROOT = 2220 660 | LETTUCE_SHOW_OPPOSING_FAKE_HAND = 2224 661 | BACON_DIABLO_FIGHT_DIABLO_PLAYER_ID = 2226 662 | LETTUCE_VERSUS_SPELL_STATE = 2228 663 | LETTUCE_START_OF_GAME_ABILITY = 2241 664 | COLOSSAL = 2247 665 | COLOSSAL_LIMB = 2248 666 | CURRENT_TEMP_SPELLPOWER_ARCANE = 2250 667 | CURRENT_TEMP_SPELLPOWER_FEL = 2251 668 | CURRENT_TEMP_SPELLPOWER_FIRE = 2252 669 | CURRENT_TEMP_SPELLPOWER_FROST = 2253 670 | CURRENT_TEMP_SPELLPOWER_NATURE = 2254 671 | CURRENT_TEMP_SPELLPOWER_HOLY = 2255 672 | CURRENT_TEMP_SPELLPOWER_PHYSICAL = 2256 673 | CURRENT_TEMP_SPELLPOWER_SHADOW = 2257 674 | CURRENT_TEMP_SPELLPOWER = 2258 675 | BACON_CHOSEN_BOARD_SKIN_ID = 2264 676 | LETTUCE_ALLIANCE = 2279 677 | LETTUCE_HORDE = 2280 678 | OBJECTIVE = 2311 679 | LETTUCE_REFRESH = 2312 680 | LETTUCE_ELVES = 2322 681 | OBJECTIVE_AURA = 2329 682 | DREDGE = 2332 683 | CURRENT_HEALING_POWER = 2333 684 | EARLY_CONCEDE_POPUP_AVAILABLE = 2340 685 | BACON_PLAYER_NUM_HERO_BUDDIES_GAINED = 2346 686 | BATTLEGROUNDS_FAVORITE_FINISHER = 2348 687 | DAMAGE_DEALT_TO_HERO_LAST_TURN = 2349 688 | LOCATION_ACTION_COST = 2352 689 | LOCATION_ACTION_COOLDOWN = 2353 690 | WHELP = 2355 691 | BACON_SPELLCRAFT_ID = 2359 692 | BACON_HERO_BUDDY_PROGRESS = 2364 693 | REVIVE = 2369 694 | BACON_HEROPOWER_BASE_HERO_ID = 2376 695 | LETTUCE_CURSED_ABILITY_VISUAL = 2381 696 | BACON_OMIT_WHEN_OUT_OF_ROTATION = 2387 697 | ALLIED = 2388 698 | LETTUCE_KEYWORD_HEALING_POWER = 2434 699 | BACON_OVERRIDE_BG_COST = 2437 700 | DONT_SUPPRESS_SUMMON_VO = 2440 701 | BACON_NUMBER_HERO_REFRESH_AVAILABLE = 2452 702 | BACON_FREEZE_TOOLTIP = 2455 703 | INFUSE = 2456 704 | INFUSED = 2457 705 | HAS_DRAG_TO_BUY = 2458 706 | ENTITY_TAG_THRESHOLD_TAG_ID = 2459 707 | ENTITY_TAG_THRESHOLD_VALUE = 2460 708 | MERCENARIES_SPELL_WEAKNESS = 2464 709 | MERCENARIES_SPELL_RESISTANCE = 2465 710 | BACON_QUESTS_ACTIVE = 2468 711 | COLOSSAL_LIMB_ON_LEFT = 2469 712 | LETTUCE_ABILITY_TILE_VISUAL_PUBLIC_SPEED = 2470 713 | BACON_DIED_LAST_COMBAT = 2483 714 | LETTUCE_ABILITY_TIER = 2493 715 | LETTUCE_EQUIPMENT_TIER = 2494 716 | MANATHIRST = 2498 717 | IMMOLATING = 2505 718 | MERCS_EXPLORER = 2510 719 | BACON_BUDDY_ENABLED = 2518 720 | BACON_EVOLUTION_CARD_ID = 2519 721 | SPELLCRAFT_HINT = 2557 722 | CORPSE = 2559 723 | MERCS_BENCH = 2570 724 | BACON_MINION_TYPE_REWARD = 2571 725 | COPIED_HINT = 2572 726 | BLEEDING = 2575 727 | HAS_SIGNATURE_QUALITY = 2589 728 | IMMOLATESTAGE = 2600 729 | EVIL_TWIN_MUSTACHE = 2611 730 | SINFUL_BRAND = 2613 731 | LETTUCE_KEYWORD_SILENCE = 2631 732 | BACON_QUEST_COMPLETED = 2633 733 | CORPSES_SPENT_THIS_GAME = 2639 734 | HAUNTED_SECRET = 2634 735 | DONT_SUPPRESS_KEYWORD_VO = 2636 736 | CARD_BACK_OVERRIDE = 2637 737 | CARDTEXT_ENTITY_0 = 2655 738 | CARDTEXT_ENTITY_1 = 2656 739 | CARDTEXT_ENTITY_2 = 2657 740 | CARDTEXT_ENTITY_3 = 2658 741 | CARDTEXT_ENTITY_4 = 2659 742 | CARDTEXT_ENTITY_5 = 2660 743 | CARDTEXT_ENTITY_6 = 2661 744 | CARDTEXT_ENTITY_7 = 2662 745 | CARDTEXT_ENTITY_8 = 2663 746 | CARDTEXT_ENTITY_9 = 2664 747 | NON_KEYWORD_SPELLBURST = 2672 748 | BACON_CARD_DBID_REWARD = 2673 749 | SECRET_LOCKED = 2676 750 | BACON_STEALTH_TOOLTIP = 2704 751 | BACON_QUEST_TOOLTIP = 2705 752 | BACON_IS_HEROPOWER_QUESTREWARD = 2706 753 | BACON_HERO_QUEST_REWARD_DATABASE_ID = 2713 754 | BACON_HERO_HEROPOWER_QUEST_REWARD_DATABASE_ID = 2714 755 | BACON_HERO_QUEST_REWARD_COMPLETED = 2715 756 | BACON_HERO_HEROPOWER_QUEST_REWARD_COMPLETED = 2716 757 | LETTUCE_FACTION = 2720 758 | DEATH_SPELL_OVERRIDE = 2722 759 | BACON_IS_BOB_QUEST = 2732 760 | BACON_HERO_REWARD_CARD_DBID = 2748 761 | BACON_HERO_HEROPOWER_REWARD_CARD_DBID = 2749 762 | BACON_HERO_REWARD_MINION_TYPE = 2750 763 | BACON_HERO_HEROPOWER_REWARD_MINION_TYPE = 2751 764 | MERCENARIES_DISCOVER_SOURCE = 2752 765 | TITAN = 2772 766 | HERO_ATTACK_GIVEN_ADDITIONAL = 2776 767 | HERO_ARMOR_GIVEN_ADDITIONAL = 2778 768 | LETTUCE_CHARGE = 2779 769 | BACON_DIED_LAST_COMBAT_HINT = 2780 770 | SHIFTING_LOCATION = 2783 771 | FORGE = 2785 772 | UNPLAYABLE_VISUALS = 2798 773 | CARDTEXT_ENTITY_AS_NUMBERS = 2802 774 | BACON_DOUBLE_QUEST_HERO_POWER = 2803 775 | MERCENARIES_TREASURE_SCALE_LEVEL = 2810 776 | FINALE = 2820 777 | OVERHEAL = 2821 778 | BACON_BLOODGEMBUFFHEALTHVALUE = 2827 779 | CARD_ALTERNATE_COST = 2837 780 | EMOTECHARACTER = 2839 781 | HAS_ACTIVATE_POWER = 2840 782 | EMOTECLASS = 2851 783 | VENOMOUS = 2853 784 | MAGNETIC_TO_RACE = 2859 785 | BACON_MAX_LEADERBOARD_ARMOR = 2867 786 | IS_USING_FORGE_OPTION = 2869 787 | BACON_REBORN_TOOLTIP = 2870 788 | BACON_PUTRICIDES_CREATION_TOOLTIP = 2875 789 | TAG_SCRIPT_DATA_NUM_3 = 2889 790 | CARD_NAME_DATA_1 = 2890 791 | BACON_GLOBAL_ANOMALY_DBID = 2897 792 | QUICKDRAW = 2905 793 | BACON_COSTS_HEALTH_TO_BUY = 2911 794 | TAG_SCRIPT_DATA_NUM_4 = 2919 795 | TAG_SCRIPT_DATA_NUM_5 = 2920 796 | TAG_SCRIPT_DATA_NUM_6 = 2921 797 | DECK_SWAP_ACTIVE = 2929 798 | MAX_SIDEBOARD_CARDS = 2931 799 | BONUS_EFFECTS = 2934 800 | BACON_USE_COIN_BASED_BUDDY_METER = 2935 801 | BACON_BUY_BUDDY = 2937 802 | BACON_BUY_BUDDY_2 = 2938 803 | BACON_DUO_TEAMMATE_PLAYER_ID = 2939 804 | BACON_SHOW_HEROPOWER_BUDDY_AS_EVOLVING_BIG_CARD = 2943 805 | HIDDEN_CHOICE_OVERRIDE = 2946 806 | BACON_DUO_PLAYER_FIGHTS_FIRST_NEXT_COMBAT = 2975 807 | NEXT_OPPONENT_TEAMMATE_PLAYER_ID = 2988 808 | BACON_CURRENT_COMBAT_PLAYER_ID = 2989 809 | FORGED = 3011 810 | BUILDING_UP = 3016 811 | BACON_PAIR_CANDIDATE = 3031 812 | CTHUN_TAUNT_BUFF = 3034 813 | BACON_TRIGGER_UPBEAT = 3046 814 | BACON_TRIGGER_XY = 3047 815 | BACON_COMBAT_PHASE_HERO = 3048 816 | FAN_LINK = 3052 817 | CTHUN_HEALTH_BUFF = 3053 818 | CTHUN_ATTACK_BUFF = 3054 819 | FORGE_REVEALED = 3070 820 | FORGES_INTO = 3074 821 | FX_DATANUM_2 = 3077 822 | RITUALIST_MINION = 3078 823 | SUPPRES_ALL_SOUNDS_FOR_ENTITY = 3093 824 | BACON_DUO_TEAM_ID = 3095 825 | FX_DATANUM_3 = 3109 826 | ALLOW_MOVE_BACON_SPELL = 3111 827 | EXCAVATE = 3114 828 | SUMMONED_WHEN_DRAWN = 3128 829 | IS_ALTERNATE_HEROPOWER = 3130 830 | TITAN_ABILITY_USED_1 = 3140 831 | TITAN_ABILITY_USED_2 = 3141 832 | TITAN_ABILITY_USED_3 = 3142 833 | BACON_DUO_TRIPLE_CANDIDATE_TEAMMATE = 3145 834 | BACON_DUO_PAIR_CANDIDATE_TEAMMATE = 3146 835 | BACON_DUO_PASSABLE = 3178 836 | ANOMALY1 = 3182 837 | ANOMALY2 = 3183 838 | IS_USING_PASS_OPTION = 3185 839 | TUTORIAL_TARGET_OPPONENT_ANIM = 3192 840 | TUTORIAL_TARGET_MINION_ANIM = 3193 841 | TUTORIAL_PLAY_MINION_ANIM = 3195 842 | TUTORIAL_HERO_POWER_TARGET_MINION_ANIM = 3196 843 | TUTORIAL_HERO_POWER_TARGET_OPPONENT_ANIM = 3197 844 | SUPPRESS_EVIL_TWIN_MUSTACHE_SOUND = 3198 845 | BACON_ANOMALY_ALL_HEROES_ARE_THIS_DBID = 3208 846 | HERO_DOESNT_MOVE_ON_ATTACK = 3211 847 | BACON_NO_TIER_UP_BUTTON = 3220 848 | TOURIST = 3228 849 | CURRENT_EXCAVATE_TIER = 3249 850 | BACON_CONSUME_TOOLTIP = 3254 851 | ALONE_RANGER = 3258 852 | CUTSCENE_CARD_TYPE = 3265 853 | MINIATURIZE = 3318 854 | MINI = 3319 855 | BACON_PASS_TOOLTIP = 3321 856 | MAX_EXCAVATE_TIER = 3326 857 | ZILLIAX_CUSTOMIZABLE_COSMETICMODULE = 3376 858 | ZILLIAX_CUSTOMIZABLE_FUNCTIONALMODULE = 3377 859 | KEEP_HERO_CLASS = 3382 860 | GIGANTIFY = 3399 861 | GIGANTIC = 3400 862 | BACON_COMBAT_DAMAGE_CAP_ENABLED = 3403 863 | BACON_TRINKET = 3407 864 | BONUS_KEYWORDS = 3423 865 | SIDEBOARD_TYPE = 3427 866 | PALADIN_AURA = 3429 867 | CREATED_BY_TWINSPELL = 3432 868 | CREATED_BY_MINIATURIZE = 3433 869 | CREATED_BY_GIGANTIFY = 3434 870 | SUPPRESS_HERO_STANDARD_SUMMON_FX = 3438 871 | ZILLIAX_CUSTOMIZABLE_LINKED_COSMETICMOUDLE = 3450 872 | BACON_SHOW_COST_ON_DISCOVER = 3456 873 | ZERG = 3457 874 | TERRAN = 3458 875 | MIN_SIDEBOARD_CARDS = 3459 876 | FORGETFUL_ATTACK_VISUAL = 3460 877 | SHUDDERWOCKHIGHLIGHTHINT = 3463 878 | PROTOSS = 3469 879 | NUM_TURNS_LAST_AFFECTED_BY = 3464 880 | EXTRA_TURNS_SPELL_OVERRIDE = 3465 881 | ZILLIAX_CUSTOMIZABLE_LINKED_FUNCTIONALMOUDLE = 3470 882 | HIDE_HEALTH_NUMBER = 3471 883 | HIDE_ATTACK_NUMBER = 3472 884 | ZILLIAX_CUSTOMIZABLE_SAVED_VERSION = 3477 885 | DUOS_QUEUED_NOT_ON_TEAM = 3478 886 | PLAYER_ABANDONED_BY_TEAMMATE = 3480 887 | SUPPRESS_MILL_ANIMATION = 3481 888 | IGNORE_SUPPRESS_MILL_ANIMATION = 3482 889 | HERO_PASSIVE_ID = 3487 890 | BACON_TEAMMATE_BONUS_MINION_DAMAGE_LAST_COMBAT = 3492 891 | BACON_DUOS_PUNISH_LEAVERS = 3494 892 | HERO_FRAME_TYPE = 3495 893 | BACON_TRIPLED_BASE_MINION_ID2 = 3499 894 | BACON_TRIPLED_BASE_MINION_ID3 = 3500 895 | QUEST_HIDE_PROGRESS = 3523 896 | STARSHIP = 3555 897 | TRANSFORM = 3562 898 | LAUNCHPAD = 3563 899 | STARSHIP_PIECE = 3568 900 | CORNER_REPLACEMENT_TYPE = 3564 901 | BACON_IS_MAGIC_ITEM_DISCOVER = 3565 902 | IS_NIGHTMARE_BONUS = 3567 903 | ROGUE_TOURIST = 3597 904 | WARLOCK_TOURIST = 3598 905 | DEATH_KNIGHT_TOURIST = 3599 906 | SHAMAN_TOURIST = 3600 907 | DEMON_HUNTER_TOURIST = 3601 908 | PRIEST_TOURIST = 3602 909 | HUNTER_TOURIST = 3603 910 | WARRIOR_TOURIST = 3604 911 | DRUID_TOURIST = 3605 912 | MAGE_TOURIST = 3606 913 | PALADIN_TOURIST = 3607 914 | IMBUE = 3626 915 | DARK_GIFT = 3627 916 | TAG_LAUNCHPAD_ABILITY = 3628 917 | STARSHIP_LAUNCH_COST_DISCOUNT = 3640 918 | BACON_DONT_SHOW_PAIR_TRIPLE_DISCOVER_VFX = 3661 919 | FALLBACK_ENCHANTMENT_PORTRAIT_DBID = 3664 920 | SHOW_SLEEP_ZZZ_OVERRIDE = 3672 921 | CLIENT_LIST_REPLACEMENTS_WHEN_PLAYED = 3677 922 | CARES_ABOUT_IMBUE_CARDS = 3692 923 | BACON_IS_POTENTIAL_TRINKET = 3705 924 | OUROBOSDEATHRATTLE = 3716 925 | BACON_TURNS_LEFT_TO_DISCOVER_TRINKET = 3738 926 | BACON_TRINKETS_ACTIVE = 3740 927 | BACON_FIRST_TRINKET_DATABASE_ID = 3741 928 | BACON_SECOND_TRINKET_DATABASE_ID = 3742 929 | BACON_HEROPOWER_TRINKET_DATABASE_ID = 3743 930 | END_OF_TURN_TRIGGER = 3744 931 | BACON_OVERRIDE_COST_COLOR = 3777 932 | BACON_EVOLUTION_CARD_ID_2 = 3778 933 | MINION_TYPE_MASK = 3802 934 | BACON_HERO_FIRST_TRINKET_LEADERBOARD_SDN1 = 3805 935 | BACON_HERO_SECOND_TRINKET_LEADERBOARD_SDN1 = 3806 936 | BACON_HERO_HEROPOWER_TRINKET_LEADERBOARD_SDN1 = 3807 937 | DEMON_PORTAL_DECK = 3808 938 | BACON_HERO_FIRST_TRINKET_LEADERBOARD_SDN2 = 3812 939 | BACON_HERO_SECOND_TRINKET_LEADERBOARD_SDN2 = 3813 940 | BACON_HERO_HEROPOWER_TRINKET_LEADERBOARD_SDN2 = 3814 941 | BACON_HERO_FIRST_TRINKET_LEADERBOARD_ALT_TEXT = 3815 942 | BACON_HERO_SECOND_TRINKET_LEADERBOARD_ALT_TEXT = 3816 943 | BACON_HERO_HEROPOWER_TRINKET_LEADERBOARD_ALT_TEXT = 3817 944 | SKIP_ARMOR_ANIMATION = 3837 945 | DRAW_SPELL_OVERRIDE = 3841 946 | BACON_NUM_MULLIGAN_REFRESH_USED = 3842 947 | DRAENEI_TRIGGER_HINT = 3847 948 | BACON_NUM_MAX_REROLL_PER_HERO = 3848 949 | MILL_SPELL_OVERRIDE = 3854 950 | DIVINE_SHIELD_DAMAGE = 3859 951 | BACON_LOCKED_MULLIGAN_HERO = 3877 952 | HAS_DARK_GIFT = 3880 953 | CREATED_AS_ON_PLAY_REPLACEMENT = 3882 954 | SHIFTING_TOP = 3894 955 | BACON_UNLOCK_MULLIGAN_HERO_ENABLED = 3896 956 | BACON_PREMIUM_FREE_REROLLS = 3901 957 | BACON_NUM_FREE_REROLLS_USED = 3904 958 | BACON_NUM_PAID_REROLLS_USED = 3905 959 | BACON_PLAYER_MULLIGAN_HERO_BEEN_REROLLED = 3906 960 | BG_COMBAT_SPEED_START_TIME = 3909 961 | BG_COMBAT_SPEED_ACCELERATION = 3910 962 | BG_COMBAT_SPEED_DECELERATION = 3911 963 | BG_COMBAT_SPEED_MAX_SPEED = 3912 964 | BG_COMBAT_SPEED_ATTACKS_REMAINING_BEFORE_SLOW_DOWN_MIN = 3913 965 | BACON_MULLIGAN_HERO_REROLL_ACTIVE = 3914 966 | ADDITIONAL_HERO_POWER_INDEX = 3919 967 | BACON_PORTAL_IN_SOLO = 3925 968 | BACON_TRIGGER_XY_STAY = 3932 969 | NON_KEYWORD_CHARGE = 3968 970 | BACON_SHOW_OVERRIDEN_MINION_COST = 3973 971 | BACON_SHOW_REFRESH_LEFT_BANNER = 3982 972 | SUPPRESS_SPELL_POWER_IN_TEXT = 3986 973 | TAVERN_SPELL_ATTACK_INCREASE = 3989 974 | TAVERN_SPELL_HEALTH_INCREASE = 3990 975 | BACON_ELEMENTAL_BUFFHEALTHVALUE = 4001 976 | BACON_ELEMENTAL_BUFFATKVALUE = 4002 977 | STARSHIP_LAUNCH_TRIGGER = 4013 978 | ADDITIONAL_HERO_POWER_ENTITY_1 = 4029 979 | IS_RELAUNCHED_STARSHIP = 4030 980 | BG_COMBAT_SPEED_MIN_ATTACKS_REMAINING_TO_START = 4035 981 | BACON_YAMATO_CANNON = 4036 982 | AGAMAGGAN_CURSE = 4056 983 | BACON_YAMATO_CANNON_TOOLTIP = 4061 984 | BACON_LIBERATOR_TOOLTIP = 4062 985 | BACON_MEDIVAC_TOOLTIP = 4063 986 | IMMUNE_TO_FIRE_SPELLS = 4066 987 | BACON_TURNS_TILL_ACTIVE = 4069 988 | BG_COMBAT_SPEED_ATTACKS_REMAINING_BEFORE_SLOW_DOWN_MAX = 4072 989 | BACON_DONT_DISPLAY_HP_IN_LEADERBOARD_OR_STATS = 4087 990 | HEROPOWER_UNLIMITED_USES = 4088 991 | ARCANE_TICKET = 4091 992 | BACON_MAGICSHOP = 4097 993 | GOLDRINN_MULTIPLIER = 4118 994 | DYNAMIC_KEYWORD1 = 4161 995 | DYNAMIC_KEYWORD2 = 4162 996 | HAMUUL_ACTIVE = 4167 997 | IMBUE_SUB_COUNTER = 4168 998 | IS_EXTRA_TRIGGERED_POWER = 4169 999 | DONT_PLAY_VFX_FROM_EXTRA_TRIGGERED_POWER = 4174 1000 | BACON_ELEMENTAL_TOOLTIP = 4197 1001 | BACON_TAVERN_SPELL_TOOLTIP = 4198 1002 | SUPPRESS_IMMOLATE_VISUAL_FOR_OPPONENT = 4213 1003 | BACON_HERO_FIRST_TRINKET_LEADERBOARD_SDN3 = 4218 1004 | BACON_HERO_SECOND_TRINKET_LEADERBOARD_SDN3 = 4219 1005 | BACON_HERO_HEROPOWER_TRINKET_LEADERBOARD_SDN3 = 4220 1006 | DISPLAY_ENTITY_IN_PLAY_ID = 4225 1007 | SUPPRESS_ALT_CARD_TEXT_FOR_OPPONENT = 4300 1008 | 1009 | InvisibleDeathrattle = 335 1010 | ImmuneToSpellpower = 349 1011 | AttackVisualType = 251 1012 | DevState = 268 1013 | GrantCharge = 355 1014 | HealTarget = 361 1015 | 1016 | # strings (all deleted?) 1017 | CARDTEXT_INHAND = CARDTEXT # But it came back... 1018 | CARDNAME = 185 1019 | ARTISTNAME = 342 1020 | FLAVORTEXT = 351 1021 | HOW_TO_EARN = 364 1022 | HOW_TO_EARN_GOLDEN = 365 1023 | CardTextInPlay = 252 1024 | TARGETING_ARROW_TEXT = 325 1025 | LocalizationNotes = 344 1026 | 1027 | # Renamed 1028 | BACON_FREEZE = BACON_FREEZE_TOOLTIP 1029 | BACON_HIGHLIGHT_ATTACKING_MINION_DURING_COMBAT = HIGHLIGHT_ATTACKING_MINION_DURING_COMBAT 1030 | BACON_USE_FAST_ANIMATIONS = USE_FAST_ACTOR_TRANSITION_ANIMATIONS 1031 | BG_COMBAT_SPEED_MIN_COMBAT_EVENTS_REMAINING_TO_START = ( 1032 | BG_COMBAT_SPEED_MIN_ATTACKS_REMAINING_TO_START 1033 | ) 1034 | CANT_BE_DAMAGED = IMMUNE 1035 | CANT_BE_DISPELLED = CANT_BE_SILENCED 1036 | CANT_BE_TARGETED_BY_ABILITIES = CANT_BE_TARGETED_BY_SPELLS 1037 | CURRENT_SPELLPOWER_BASE = CURRENT_SPELLPOWER 1038 | CURRENT_TEMP_SPELLPOWER_BASE = CURRENT_TEMP_SPELLPOWER 1039 | DEATH_RATTLE = DEATHRATTLE 1040 | DEATHRATTLE_SENDS_BACK_TO_DECK = DEATHRATTLE_RETURN_ZONE 1041 | DISABLE_GOLDEN_ANIMATIONS = DISABLE_NONHERO_GOLDEN_ANIMATIONS 1042 | DURABILITY_DEPRECATED = DURABILITY 1043 | EXTRA_DEATHRATTLES = EXTRA_MINION_DEATHRATTLES_BASE 1044 | HAND_REVEALED = ZONES_REVEALED 1045 | HEALING_DOUBLE = SPELL_HEALING_DOUBLE 1046 | # HIDE_COST = HIDE_STATS # Added back 1047 | KAZAKUS_POTION_POWER_1 = MODULAR_ENTITY_PART_1 1048 | KAZAKUS_POTION_POWER_2 = MODULAR_ENTITY_PART_2 1049 | LINKEDCARD = LINKED_ENTITY 1050 | MODULAR = MAGNETIC 1051 | RECALL = OVERLOAD 1052 | RECALL_OWED = OVERLOAD_OWED 1053 | RED_MANA_CRYSTALS = RED_MANA_GEM 1054 | TAG_HERO_POWER_DOUBLE = HERO_POWER_DOUBLE 1055 | TAG_AI_MUST_PLAY = AI_MUST_PLAY 1056 | # TREASURE = DISCOVER # Added back 1057 | SHOWN_HERO_POWER = HERO_POWER 1058 | EVILZUG = MARK_OF_EVIL 1059 | TRADE_COST = DECK_ACTION_COST 1060 | START_OF_GAME = START_OF_GAME_KEYWORD 1061 | SPELLRESISTANCE_ARCANE = SPELL_RESISTANCE_ARCANE 1062 | SPELLRESISTANCE_FIRE = SPELL_RESISTANCE_FIRE 1063 | SPELLRESISTANCE_FROST = SPELL_RESISTANCE_FROST 1064 | SPELLRESISTANCE_NATURE = SPELL_RESISTANCE_NATURE 1065 | SPELLRESISTANCE_HOLY = SPELL_RESISTANCE_HOLY 1066 | SPELLRESISTANCE_SHADOW = SPELL_RESISTANCE_SHADOW 1067 | SPELLRESISTANCE_FEL = SPELL_RESISTANCE_FEL 1068 | SPELLWEAKNESS_ARCANE = SPELL_WEAKNESS_ARCANE 1069 | SPELLWEAKNESS_FIRE = SPELL_WEAKNESS_FIRE 1070 | SPELLWEAKNESS_FROST = SPELL_WEAKNESS_FROST 1071 | SPELLWEAKNESS_NATURE = SPELL_WEAKNESS_NATURE 1072 | SPELLWEAKNESS_HOLY = SPELL_WEAKNESS_HOLY 1073 | SPELLWEAKNESS_SHADOW = SPELL_WEAKNESS_SHADOW 1074 | SPELLWEAKNESS_FEL = SPELL_WEAKNESS_FEL 1075 | LETTUCE_ATTACK = LETTUCE_KEYWORD_ATTACK 1076 | LETTUCE_SPELLCOMBO = LETTUCE_KEYWORD_SPELL_COMBO 1077 | BLEED = LETTUCE_BLEED 1078 | CRITICALDAMAGE = LETTUCE_KEYWORD_CRITICAL_DAMAGE 1079 | ROOT = LETTUCE_KEYWORD_ROOT 1080 | LETTUCE_HEALINGPOWER = LETTUCE_KEYWORD_HEALING_POWER 1081 | MERCS_SPELLWEAKNESS = MERCENARIES_SPELL_WEAKNESS 1082 | MERCS_SPELLRESISTANCE = MERCENARIES_SPELL_RESISTANCE 1083 | LETTUCE_SILENCE = LETTUCE_KEYWORD_SILENCE 1084 | ENRAGED_TOOLTIP = ENRAGE_TOOLTIP 1085 | SIDEQUEST = SIDE_QUEST 1086 | AUTOATTACK = AUTO_ATTACK 1087 | CASTSWHENDRAWN = CASTS_WHEN_DRAWN 1088 | FATIGUEREFERENCE = FATIGUE_REFERENCE 1089 | CORRUPTEDCARD = CORRUPTED_CARD 1090 | HONORABLEKILL = HONORABLE_KILL 1091 | BONUSEFFECTS = BONUS_EFFECTS 1092 | BLOOD_GEM = BACON_BLOOD_GEM_TOOLTIP 1093 | REFRESH = BACON_REFRESH_TOOLTIP 1094 | SPELLCRAFT = BACON_SPELLCRAFT_ID 1095 | BACON_PUTRICIDESCREATION_TOOLTIP = BACON_PUTRICIDES_CREATION_TOOLTIP 1096 | TWINSPELL_COPY = DEPRECATED_TWINSPELL_COPY 1097 | 1098 | # Deleted 1099 | IGNORE_DAMAGE = 1 1100 | GOLD_REWARD_STATE = 13 1101 | COPY_DEATHRATTLE = 55 1102 | COPY_DEATHRATTLE_INDEX = 56 1103 | CARD_ID = 186 1104 | INCOMING_HEALING_MULTIPLIER = 233 1105 | INCOMING_HEALING_ADJUSTMENT = 234 1106 | INCOMING_HEALING_CAP = 235 1107 | INCOMING_DAMAGE_MULTIPLIER = 236 1108 | INCOMING_DAMAGE_ADJUSTMENT = 237 1109 | INCOMING_DAMAGE_CAP = 238 1110 | OUTGOING_DAMAGE_CAP = 273 1111 | OUTGOING_DAMAGE_ADJUSTMENT = 274 1112 | OUTGOING_DAMAGE_MULTIPLIER = 275 1113 | OUTGOING_HEALING_CAP = 276 1114 | OUTGOING_HEALING_ADJUSTMENT = 277 1115 | OUTGOING_HEALING_MULTIPLIER = 278 1116 | INCOMING_ABILITY_DAMAGE_ADJUSTMENT = 279 1117 | INCOMING_COMBAT_DAMAGE_ADJUSTMENT = 280 1118 | OUTGOING_ABILITY_DAMAGE_ADJUSTMENT = 281 1119 | OUTGOING_COMBAT_DAMAGE_ADJUSTMENT = 282 1120 | OUTGOING_ABILITY_DAMAGE_MULTIPLIER = 283 1121 | OUTGOING_ABILITY_DAMAGE_CAP = 284 1122 | INCOMING_ABILITY_DAMAGE_MULTIPLIER = 285 1123 | INCOMING_ABILITY_DAMAGE_CAP = 286 1124 | OUTGOING_COMBAT_DAMAGE_MULTIPLIER = 287 1125 | OUTGOING_COMBAT_DAMAGE_CAP = 288 1126 | INCOMING_COMBAT_DAMAGE_MULTIPLIER = 289 1127 | INCOMING_COMBAT_DAMAGE_CAP = 290 1128 | DIVINE_SHIELD_READY = 314 1129 | IGNORE_DAMAGE_OFF = 354 1130 | NUM_OPTIONS = 359 1131 | LAST_CARD_PLAYED = 397 1132 | RITUAL = 424 1133 | PROXY_CTHUN = 434 1134 | PENDING_EVOLUTIONS = 461 1135 | MULTI_CLASS_GROUP = 480 1136 | WEATHER = 1002 1137 | WEATHERSNOWSTORM = 1012 1138 | WEATHERTHUNDERSTORM = 1013 1139 | WEATHERFIRESTORM = 1014 1140 | EXTRA_SPELL_CASTS_BASE = 1140 1141 | EXTRA_OVERLOAD_SPELL_CASTS_BASE = 1272 1142 | EXTRA_SPELL_CASTS_ADDITIONAL = 1348 1143 | BACON_MINION_IS_LEVEL_TWO = 1421 1144 | PIECE_OF_CTHUN = 1477 1145 | AVFACTION = 2323 1146 | AVRANK = 2324 1147 | MERCS_DISCOVER = 2665 1148 | TOPDECK = 377 1149 | DECK_RULE_COUNT_AS_COPY_OF_CARD_ID = 1413 1150 | CARD_COSTS_HEALTH = 481 1151 | CARD_COSTS_ARMOR = 2811 1152 | 1153 | # Missing/guessed, only present in logs 1154 | # Note: the names of these can change at any time! 1155 | WEAPON = 334 1156 | DISCARD_CARDS = 890 1157 | BATTLEGROUNDS_HERO_ARMOR_TIER = 1723 1158 | BATTLEGROUNDS_DARKMOON_PRIZE_TURN = 1735 1159 | EXCAVATE_COUNTER = 2822 1160 | BACON_ELEMENTAL_PLAY_COUNTER = 2878 1161 | IS_BACON_POOL_SPELL = 3081 1162 | IS_BACON_DUOS_EXCLUSIVE = 3166 1163 | 1164 | CANT_BE_EXHAUSTED = 244 1165 | CANT_EXHAUST = 226 1166 | CANT_TARGET = 228 1167 | CANT_DESTROY = 229 1168 | 1169 | UPGRADED_HERO_POWER = 1086 1170 | LIBRAM = 1546 1171 | SI_7 = 1678 1172 | 1173 | # Enum number changed 1174 | # HISTORY_PROXY_NO_BIG_CARD = 427 1175 | 1176 | @property 1177 | def type(self): 1178 | return TAG_TYPES.get(self, Type.NUMBER) 1179 | 1180 | @property 1181 | def string_type(self): 1182 | return self.type in (Type.LOCSTRING, Type.STRING) 1183 | 1184 | 1185 | TAG_NAMES = { 1186 | GameTag.TRIGGER_VISUAL: "TriggerVisual", 1187 | GameTag.HEALTH: "Health", 1188 | GameTag.ATK: "Atk", 1189 | GameTag.COST: "Cost", 1190 | GameTag.ELITE: "Elite", 1191 | GameTag.CARD_SET: "CardSet", 1192 | GameTag.CARDTEXT_INHAND: "CardTextInHand", 1193 | GameTag.CARDNAME: "CardName", 1194 | GameTag.DURABILITY: "Durability", 1195 | GameTag.WINDFURY: "Windfury", 1196 | GameTag.TAUNT: "Taunt", 1197 | GameTag.STEALTH: "Stealth", 1198 | GameTag.SPELLPOWER: "Spellpower", 1199 | GameTag.DIVINE_SHIELD: "Divine Shield", 1200 | GameTag.CHARGE: "Charge", 1201 | GameTag.CLASS: "Class", 1202 | GameTag.CARDRACE: "Race", 1203 | GameTag.FACTION: "Faction", 1204 | GameTag.RARITY: "Rarity", 1205 | GameTag.CARDTYPE: "CardType", 1206 | GameTag.FREEZE: "Freeze", 1207 | GameTag.ENRAGED: "Enrage", 1208 | GameTag.RECALL: "Recall", 1209 | GameTag.DEATHRATTLE: "Deathrattle", 1210 | GameTag.BATTLECRY: "Battlecry", 1211 | GameTag.SECRET: "Secret", 1212 | GameTag.COMBO: "Combo", 1213 | GameTag.IMMUNE: "Cant Be Damaged", 1214 | GameTag.AttackVisualType: "AttackVisualType", 1215 | GameTag.CardTextInPlay: "CardTextInPlay", 1216 | GameTag.DevState: "DevState", 1217 | GameTag.MORPH: "Morph", 1218 | GameTag.COLLECTIBLE: "Collectible", 1219 | GameTag.TARGETING_ARROW_TEXT: "TargetingArrowText", 1220 | GameTag.ENCHANTMENT_BIRTH_VISUAL: "EnchantmentBirthVisual", 1221 | GameTag.ENCHANTMENT_IDLE_VISUAL: "EnchantmentIdleVisual", 1222 | GameTag.InvisibleDeathrattle: "InvisibleDeathrattle", 1223 | GameTag.TAG_ONE_TURN_EFFECT: "OneTurnEffect", 1224 | GameTag.SILENCE: "Silence", 1225 | GameTag.COUNTER: "Counter", 1226 | GameTag.ARTISTNAME: "ArtistName", 1227 | GameTag.ImmuneToSpellpower: "ImmuneToSpellpower", 1228 | GameTag.ADJACENT_BUFF: "AdjacentBuff", 1229 | GameTag.FLAVORTEXT: "FlavorText", 1230 | GameTag.HealTarget: "HealTarget", 1231 | GameTag.AURA: "Aura", 1232 | GameTag.POISONOUS: "Poisonous", 1233 | GameTag.HOW_TO_EARN: "HowToGetThisCard", 1234 | GameTag.HOW_TO_EARN_GOLDEN: "HowToGetThisGoldCard", 1235 | GameTag.AI_MUST_PLAY: "AIMustPlay", 1236 | GameTag.AFFECTED_BY_SPELL_POWER: "AffectedBySpellPower", 1237 | GameTag.SPARE_PART: "SparePart", 1238 | GameTag.HIDE_STATS: "HideStats", 1239 | GameTag.DISCOVER: "Treasure", 1240 | GameTag.AUTO_ATTACK: "AutoAttack", 1241 | } 1242 | 1243 | 1244 | ## 1245 | # Card enums 1246 | 1247 | class CardClass(IntEnum): 1248 | """TAG_CLASS""" 1249 | 1250 | INVALID = 0 1251 | DEATHKNIGHT = 1 1252 | DRUID = 2 1253 | HUNTER = 3 1254 | MAGE = 4 1255 | PALADIN = 5 1256 | PRIEST = 6 1257 | ROGUE = 7 1258 | SHAMAN = 8 1259 | WARLOCK = 9 1260 | WARRIOR = 10 1261 | DREAM = 11 1262 | NEUTRAL = 12 1263 | WHIZBANG = 13 1264 | DEMONHUNTER = 14 1265 | 1266 | @property 1267 | def default_hero(self): 1268 | from .utils import CARDCLASS_HERO_MAP 1269 | return CARDCLASS_HERO_MAP.get(self, "") 1270 | 1271 | @property 1272 | def is_playable(self): 1273 | return self != CardClass.WHIZBANG and self.default_hero 1274 | 1275 | @property 1276 | def visiting_tourists(self): 1277 | from .utils import VISITING_TOURISTS 1278 | return VISITING_TOURISTS.get(self, []) 1279 | 1280 | @property 1281 | def name_global(self): 1282 | return "GLOBAL_CLASS_%s" % (self.name) 1283 | 1284 | 1285 | class CardSet(IntEnum): 1286 | """TAG_CARD_SET""" 1287 | 1288 | INVALID = 0 1289 | TEST_TEMPORARY = 1 1290 | BASIC = 2 1291 | EXPERT1 = 3 1292 | HOF = 4 1293 | MISSIONS = 5 1294 | DEMO = 6 1295 | NONE = 7 1296 | CHEAT = 8 1297 | BLANK = 9 1298 | DEBUG_SP = 10 1299 | PROMO = 11 1300 | NAXX = 12 # Curse of Naxxramas 1301 | GVG = 13 # Goblins vs Gnomes 1302 | BRM = 14 # Blackrock Mountain 1303 | TGT = 15 # The Grand Tournament 1304 | CREDITS = 16 1305 | HERO_SKINS = 17 1306 | TB = 18 # Tavern Brawl 1307 | SLUSH = 19 1308 | LOE = 20 # The League of Explorers 1309 | OG = 21 # Whispers of the Old Gods 1310 | OG_RESERVE = 22 1311 | KARA = 23 # One Night in Karazhan 1312 | KARA_RESERVE = 24 1313 | GANGS = 25 # Mean Streets of Gadgetzan 1314 | GANGS_RESERVE = 26 1315 | UNGORO = 27 # Journey to Un'Goro 1316 | ICECROWN = 1001 # Knights of the Frozen Throne 1317 | TB_DEV = 1003 1318 | LOOTAPALOOZA = 1004 # Kobolds & Catacombs 1319 | GILNEAS = 1125 # The Witchwood 1320 | BOOMSDAY = 1127 # The Boomsday Project 1321 | TROLL = 1129 # Rastakhan's Rumble 1322 | DALARAN = 1130 # Rise of Shadows 1323 | ULDUM = 1158 # Saviours of Uldum 1324 | DRAGONS = 1347 # Descent of Dragons 1325 | YEAR_OF_THE_DRAGON = 1403 1326 | BLACK_TEMPLE = 1414 # Ashes of Outlands 1327 | WILD_EVENT = 1439 1328 | SCHOLOMANCE = 1443 # Scholomance Academy 1329 | BATTLEGROUNDS = 1453 1330 | DEMON_HUNTER_INITIATE = 1463 1331 | DARKMOON_FAIRE = 1466 # Madness at the Darkmoon Faire 1332 | THE_BARRENS = 1525 # Forged in the Barrens 1333 | WAILING_CAVERNS = 1559 1334 | STORMWIND = 1578 # United in Stormwind 1335 | LETTUCE = 1586 # Mercenaries 1336 | ALTERAC_VALLEY = 1626 # Fractured in Alterac Valley 1337 | LEGACY = 1635 1338 | CORE = 1637 1339 | VANILLA = 1646 1340 | THE_SUNKEN_CITY = 1658 # Voyage to the Sunken City 1341 | REVENDRETH = 1691 # Murder at Castle Nathria 1342 | MERCENARIES_DEV = 1705 1343 | RETURN_OF_THE_LICH_KING = 1776 1344 | BATTLE_OF_THE_BANDS = 1809 1345 | TITANS = 1858 1346 | PATH_OF_ARTHAS = 1869 1347 | WILD_WEST = 1892 1348 | WONDERS = 1898 1349 | WHIZBANGS_WORKSHOP = 1897 1350 | TUTORIAL = 1904 1351 | ISLAND_VACATION = 1905 # Perils in Paradise 1352 | SPACE = 1935 # Great Dark Beyond 1353 | EVENT = 1941 1354 | EMERALD_DREAM = 1946 1355 | THE_LOST_CITY = 1952 1356 | 1357 | # Not actually present... 1358 | TAVERNS_OF_TIME = 1143 1359 | PLACEHOLDER_202204 = 1810 1360 | 1361 | # Aliased from the original enums 1362 | FP1 = 12 1363 | PE1 = 13 1364 | 1365 | # Renamed 1366 | FP2 = BRM 1367 | PE2 = TEMP1 = TGT 1368 | REWARD = HOF 1369 | 1370 | @property 1371 | def craftable(self): 1372 | return self in ( 1373 | CardSet.NAXX, 1374 | CardSet.GVG, 1375 | CardSet.BRM, 1376 | CardSet.TGT, 1377 | CardSet.LOE, 1378 | CardSet.OG, 1379 | CardSet.KARA, 1380 | CardSet.GANGS, 1381 | CardSet.UNGORO, 1382 | CardSet.ICECROWN, 1383 | CardSet.LOOTAPALOOZA, 1384 | CardSet.GILNEAS, 1385 | CardSet.BOOMSDAY, 1386 | CardSet.TROLL, 1387 | CardSet.DALARAN, 1388 | CardSet.ULDUM, 1389 | CardSet.DRAGONS, 1390 | CardSet.BLACK_TEMPLE, 1391 | CardSet.DEMON_HUNTER_INITIATE, 1392 | CardSet.SCHOLOMANCE, 1393 | CardSet.DARKMOON_FAIRE, 1394 | CardSet.THE_BARRENS, 1395 | CardSet.ALTERAC_VALLEY, 1396 | CardSet.LEGACY, 1397 | CardSet.THE_SUNKEN_CITY, 1398 | CardSet.RETURN_OF_THE_LICH_KING, 1399 | CardSet.PATH_OF_ARTHAS, 1400 | CardSet.BATTLE_OF_THE_BANDS, 1401 | CardSet.TITANS, 1402 | CardSet.WILD_WEST, 1403 | CardSet.WONDERS, 1404 | CardSet.WHIZBANGS_WORKSHOP, 1405 | CardSet.ISLAND_VACATION, 1406 | CardSet.SPACE, 1407 | CardSet.EMERALD_DREAM, 1408 | CardSet.THE_LOST_CITY, 1409 | ) 1410 | 1411 | @property 1412 | def name_global(self): 1413 | # Newer sets use a 2-3 letter set code 1414 | from .utils import CARDSET_GLOBAL_STRING_MAP 1415 | custom = CARDSET_GLOBAL_STRING_MAP.get(self) 1416 | if custom: 1417 | return custom 1418 | 1419 | # Older sets used the enum name 1420 | return "GLOBAL_CARD_SET_%s" % (self.name) 1421 | 1422 | @property 1423 | def short_name_global(self): 1424 | return self.name_global + "_SHORT" 1425 | 1426 | @property 1427 | def is_standard(self): 1428 | return self in ZodiacYear.PEGASUS.standard_card_sets 1429 | 1430 | 1431 | class CardType(IntEnum): 1432 | """TAG_CARDTYPE""" 1433 | 1434 | INVALID = 0 1435 | GAME = 1 1436 | PLAYER = 2 1437 | HERO = 3 1438 | MINION = 4 1439 | SPELL = 5 1440 | ENCHANTMENT = 6 1441 | WEAPON = 7 1442 | ITEM = 8 1443 | TOKEN = 9 1444 | HERO_POWER = 10 1445 | BLANK = 11 1446 | GAME_MODE_BUTTON = 12 1447 | MOVE_MINION_HOVER_TARGET = 22 1448 | LETTUCE_ABILITY = 23 1449 | BATTLEGROUND_HERO_BUDDY = 24 1450 | LOCATION = 39 1451 | BATTLEGROUND_QUEST_REWARD = 40 1452 | BATTLEGROUND_SPELL = 42 1453 | BATTLEGROUND_ANOMALY = 43 1454 | BATTLEGROUND_TRINKET = 44 1455 | 1456 | # Renamed 1457 | ABILITY = SPELL 1458 | 1459 | @property 1460 | def playable(self): 1461 | return self in ( 1462 | CardType.HERO, 1463 | CardType.MINION, 1464 | CardType.SPELL, 1465 | CardType.WEAPON, 1466 | CardType.LOCATION, 1467 | ) 1468 | 1469 | @property 1470 | def craftable(self): 1471 | return self in ( 1472 | CardType.HERO, 1473 | CardType.MINION, 1474 | CardType.SPELL, 1475 | CardType.WEAPON, 1476 | CardType.LOCATION, 1477 | ) 1478 | 1479 | @property 1480 | def name_global(self): 1481 | if self.name == "HERO_POWER": 1482 | return "GLOBAL_CARDTYPE_HEROPOWER" 1483 | return "GLOBAL_CARDTYPE_%s" % (self.name) 1484 | 1485 | 1486 | class EnchantmentVisual(IntEnum): 1487 | """TAG_ENCHANTMENT_VISUAL""" 1488 | 1489 | INVALID = 0 1490 | POSITIVE = 1 1491 | NEGATIVE = 2 1492 | NEUTRAL = 3 1493 | 1494 | 1495 | class Faction(IntEnum): 1496 | """TAG_FACTION""" 1497 | 1498 | INVALID = 0 1499 | HORDE = 1 1500 | ALLIANCE = 2 1501 | NEUTRAL = 3 1502 | 1503 | 1504 | class Race(IntEnum): 1505 | """TAG_RACE""" 1506 | 1507 | INVALID = 0 1508 | BLOODELF = 1 1509 | DRAENEI = 2 1510 | DWARF = 3 1511 | GNOME = 4 1512 | GOBLIN = 5 1513 | HUMAN = 6 1514 | NIGHTELF = 7 1515 | ORC = 8 1516 | TAUREN = 9 1517 | TROLL = 10 1518 | UNDEAD = 11 1519 | WORGEN = 12 1520 | GOBLIN2 = 13 1521 | MURLOC = 14 1522 | DEMON = 15 1523 | SCOURGE = 16 1524 | MECHANICAL = 17 1525 | ELEMENTAL = 18 1526 | OGRE = 19 1527 | BEAST = 20 1528 | TOTEM = 21 1529 | NERUBIAN = 22 1530 | PIRATE = 23 1531 | DRAGON = 24 1532 | BLANK = 25 1533 | ALL = 26 1534 | EGG = 38 1535 | QUILBOAR = 43 1536 | CENTAUR = 80 1537 | FURBOLG = 81 1538 | HIGHELF = 83 1539 | TREANT = 84 1540 | OWLKIN = 85 1541 | HALFORC = 88 1542 | LOCK = 89 1543 | NAGA = 92 1544 | OLDGOD = 93 1545 | PANDAREN = 94 1546 | GRONN = 95 1547 | CELESTIAL = 96 1548 | GNOLL = 97 1549 | GOLEM = 98 1550 | HARPY = 99 1551 | VULPERA = 100 1552 | # When adding a new race, ensure you also update utils.RACE_TAG_MAP 1553 | 1554 | # Aliased 1555 | PET = 20 1556 | 1557 | @property 1558 | def name_global(self): 1559 | if self.name == "BEAST": 1560 | return "GLOBAL_RACE_PET" 1561 | return "GLOBAL_RACE_%s" % (self.name) 1562 | 1563 | @property 1564 | def visible(self): 1565 | # XXX: Mech is only a visible tribe since GVG 1566 | return self in VISIBLE_RACES 1567 | 1568 | @property 1569 | def is_battlegrounds_pool(self): 1570 | """Whether this Race appears as a minion pool in Battlegrounds matches.""" 1571 | return self in BATTLEGROUNDS_RACES 1572 | 1573 | @property 1574 | def race_tag(self): 1575 | from .utils import CARDRACE_TAG_MAP 1576 | return CARDRACE_TAG_MAP.get(self) 1577 | 1578 | @staticmethod 1579 | def get_race_for_game_tag(game_tag): 1580 | from .utils import REVERSE_CARDRACE_TAG_MAP 1581 | return REVERSE_CARDRACE_TAG_MAP.get(game_tag) 1582 | 1583 | @property 1584 | def text_order(self): 1585 | return ( 1586 | RACE_TEXT_ORDER.index(self) if self in RACE_TEXT_ORDER 1587 | # Sort the others to the end, but keep enum order 1588 | else len(RACE_TEXT_ORDER) + int(self) 1589 | ) 1590 | 1591 | 1592 | VISIBLE_RACES = [ 1593 | Race.MURLOC, Race.DEMON, Race.MECHANICAL, Race.ELEMENTAL, Race.BEAST, 1594 | Race.TOTEM, Race.PIRATE, Race.DRAGON, Race.ALL, 1595 | ] 1596 | 1597 | # The order in which the races appear on cards 1598 | RACE_TEXT_ORDER = [ 1599 | Race.UNDEAD, Race.ELEMENTAL, Race.MECHANICAL, Race.DEMON, Race.MURLOC, Race.QUILBOAR, 1600 | Race.NAGA, Race.PET, Race.DRAGON, Race.TOTEM, Race.PIRATE 1601 | ] 1602 | 1603 | # All minion types that may appear as a minion pool in Battlegrounds matches. 1604 | # As of May 2022 matches will always contain five of these. Some are guaranteed to appear 1605 | # in every match. 1606 | BATTLEGROUNDS_RACES = [ 1607 | Race.MURLOC, Race.DEMON, Race.MECHANICAL, Race.BEAST, Race.DRAGON, 1608 | Race.PIRATE, Race.ELEMENTAL, Race.QUILBOAR, Race.NAGA, Race.UNDEAD, 1609 | ] 1610 | 1611 | 1612 | class Rarity(IntEnum): 1613 | """TAG_RARITY""" 1614 | 1615 | INVALID = 0 1616 | COMMON = 1 1617 | FREE = 2 1618 | RARE = 3 1619 | EPIC = 4 1620 | LEGENDARY = 5 1621 | 1622 | # TB_BlingBrawl_Blade1e (10956) 1623 | UNKNOWN_6 = 6 1624 | 1625 | @property 1626 | def craftable(self): 1627 | return self in ( 1628 | Rarity.COMMON, 1629 | Rarity.RARE, 1630 | Rarity.EPIC, 1631 | Rarity.LEGENDARY, 1632 | ) 1633 | 1634 | @property 1635 | def crafting_costs(self): 1636 | from .utils import CRAFTING_COSTS 1637 | return CRAFTING_COSTS.get(self, (0, 0)) 1638 | 1639 | @property 1640 | def disenchant_costs(self): 1641 | from .utils import DISENCHANT_COSTS 1642 | return DISENCHANT_COSTS.get(self, (0, 0)) 1643 | 1644 | @property 1645 | def name_global(self): 1646 | return "GLOBAL_RARITY_%s" % (self.name) 1647 | 1648 | 1649 | class Zone(IntEnum): 1650 | """TAG_ZONE""" 1651 | 1652 | INVALID = 0 1653 | PLAY = 1 1654 | DECK = 2 1655 | HAND = 3 1656 | GRAVEYARD = 4 1657 | REMOVEDFROMGAME = 5 1658 | SETASIDE = 6 1659 | SECRET = 7 1660 | LETTUCE_ABILITY = 8 1661 | 1662 | # Renamed 1663 | 1664 | STUB_ZONE_8 = LETTUCE_ABILITY 1665 | 1666 | 1667 | # While the Role enum has a generic name, as of October 2021 it is exclusive to the 1668 | # Mercenaries/Lettuce game mode. 1669 | class Role(IntEnum): 1670 | """TAG_ROLE""" 1671 | 1672 | INVALID = 0 1673 | CASTER = 1 1674 | FIGHTER = 2 1675 | TANK = 3 1676 | NEUTRAL = 4 1677 | 1678 | 1679 | ## 1680 | # Game enums 1681 | 1682 | class ChoiceType(IntEnum): 1683 | """CHOICE_TYPE""" 1684 | 1685 | INVALID = 0 1686 | MULLIGAN = 1 1687 | GENERAL = 2 1688 | TARGET = 3 1689 | 1690 | 1691 | class BnetGameType(IntEnum): 1692 | """PegasusShared.BnetGameType""" 1693 | BGT_UNKNOWN = 0 1694 | BGT_FRIENDS = 1 1695 | BGT_RANKED_STANDARD = 2 1696 | BGT_ARENA = 3 1697 | BGT_VS_AI = 4 1698 | BGT_TUTORIAL = 5 1699 | BGT_ASYNC = 6 1700 | BGT_CASUAL_STANDARD_NEWBIE = 9 1701 | BGT_CASUAL_STANDARD_NORMAL = 10 1702 | BGT_TEST1 = 11 1703 | BGT_TEST2 = 12 1704 | BGT_TEST3 = 13 1705 | BGT_TAVERNBRAWL_PVP = 16 1706 | BGT_TAVERNBRAWL_1P_VERSUS_AI = 17 1707 | BGT_TAVERNBRAWL_2P_COOP = 18 1708 | BGT_RANKED_WILD = 30 1709 | BGT_CASUAL_WILD = 31 1710 | BGT_FSG_BRAWL_VS_FRIEND = 40 1711 | BGT_FSG_BRAWL_PVP = 41 1712 | BGT_FSG_BRAWL_1P_VERSUS_AI = 42 1713 | BGT_FSG_BRAWL_2P_COOP = 43 1714 | BGT_RANKED_STANDARD_NEW_PLAYER = 45 1715 | BGT_BATTLEGROUNDS = 50 1716 | BGT_BATTLEGROUNDS_FRIENDLY = 51 1717 | BGT_PVPDR_PAID = 54 1718 | BGT_PVPDR = 55 1719 | BGT_MERCENARIES_PVP = 56 1720 | BGT_MERCENARIES_PVE = 57 1721 | BGT_RANKED_CLASSIC = 58 1722 | BGT_CASUAL_CLASSIC = 59 1723 | BGT_MERCENARIES_PVE_COOP = 60 1724 | BGT_MERCENARIES_FRIENDLY = 61 1725 | BGT_BATTLEGROUNDS_PLAYER_VS_AI = 62, 1726 | BGT_RANKED_TWIST = 63, 1727 | BGT_CASUAL_TWIST = 64, 1728 | BGT_BATTLEGROUNDS_DUO = 65 1729 | BGT_BATTLEGROUNDS_DUO_VS_AI = 66 1730 | BGT_BATTLEGROUNDS_DUO_FRIENDLY = 67 1731 | BGT_CASUAL_STANDARD_APPRENTICE = 68 1732 | BGT_UNDERGROUND_ARENA = 73 1733 | BGT_LAST = 74 1734 | # BGT_LAST = 65 1735 | 1736 | BGT_NEWBIE = BGT_CASUAL_STANDARD_NEWBIE 1737 | BGT_CASUAL_STANDARD = BGT_CASUAL_STANDARD_NORMAL 1738 | 1739 | BGT_RESERVED_18_22 = BGT_MERCENARIES_PVP 1740 | BGT_RESERVED_18_23 = BGT_MERCENARIES_PVE 1741 | 1742 | # Removed 1743 | # BGT_TOURNAMENT = 44 1744 | 1745 | 1746 | ARENA_GAME_TYPES = [ 1747 | BnetGameType.BGT_ARENA, 1748 | BnetGameType.BGT_UNDERGROUND_ARENA 1749 | ] 1750 | 1751 | CLASSIC_GAME_TYPES = [ 1752 | BnetGameType.BGT_CASUAL_CLASSIC, 1753 | BnetGameType.BGT_RANKED_CLASSIC 1754 | ] 1755 | 1756 | STANDARD_GAME_TYPES = [ 1757 | BnetGameType.BGT_CASUAL_STANDARD, 1758 | BnetGameType.BGT_RANKED_STANDARD, 1759 | ] 1760 | 1761 | TWIST_GAME_TYPES = [ 1762 | BnetGameType.BGT_CASUAL_TWIST, 1763 | BnetGameType.BGT_RANKED_TWIST, 1764 | ] 1765 | 1766 | WILD_GAME_TYPES = [ 1767 | BnetGameType.BGT_CASUAL_WILD, 1768 | BnetGameType.BGT_RANKED_WILD, 1769 | ] 1770 | 1771 | 1772 | class FormatType(IntEnum): 1773 | """PegasusShared.FormatType""" 1774 | 1775 | FT_UNKNOWN = 0 1776 | FT_WILD = 1 1777 | FT_STANDARD = 2 1778 | FT_CLASSIC = 3 1779 | FT_TWIST = 4 1780 | 1781 | @property 1782 | def name_global(self): 1783 | if self.name == "FT_WILD": 1784 | return "GLOBAL_WILD" 1785 | elif self.name == "FT_STANDARD": 1786 | return "GLOBAL_STANDARD" 1787 | elif self.name == "FT_CLASSIC": 1788 | return "GLOBAL_CLASSIC" 1789 | elif self.name == "FT_TWIST": 1790 | return "GLOBAL_TWIST" 1791 | 1792 | 1793 | class GameType(IntEnum): 1794 | """PegasusShared.GameType""" 1795 | GT_UNKNOWN = 0 1796 | GT_VS_AI = 1 1797 | GT_VS_FRIEND = 2 1798 | GT_TUTORIAL = 4 1799 | GT_ARENA = 5 1800 | GT_TEST_AI_VS_AI = 6 1801 | GT_RANKED = 7 1802 | GT_CASUAL = 8 1803 | GT_TAVERNBRAWL = 16 1804 | GT_TB_1P_VS_AI = 17 1805 | GT_TB_2P_COOP = 18 1806 | GT_FSG_BRAWL_VS_FRIEND = 19 1807 | GT_FSG_BRAWL = 20 1808 | GT_FSG_BRAWL_1P_VS_AI = 21 1809 | GT_FSG_BRAWL_2P_COOP = 22 1810 | GT_BATTLEGROUNDS = 23 1811 | GT_BATTLEGROUNDS_FRIENDLY = 24 1812 | GT_PVPDR_PAID = 28 1813 | GT_PVPDR = 29 1814 | GT_MERCENARIES_PVP = 30 1815 | GT_MERCENARIES_PVE = 31 1816 | GT_MERCENARIES_PVE_COOP = 32 1817 | GT_MERCENARIES_AI_VS_AI = 33 1818 | GT_MERCENARIES_FRIENDLY = 34 1819 | GT_BATTLEGROUNDS_AI_VS_AI = 35 1820 | GT_BATTLEGROUNDS_PLAYER_VS_AI = 36 1821 | GT_BATTLEGROUNDS_DUO = 37 1822 | GT_BATTLEGROUNDS_DUO_VS_AI = 38 1823 | GT_BATTLEGROUNDS_DUO_FRIENDLY = 39 1824 | GT_BATTLEGROUNDS_DUO_AI_VS_AI = 40 1825 | GT_BATTLEGROUNDS_DUO_1_PLAYER_VS_AI = 41 1826 | GT_UNDERGROUND_ARENA = 42 1827 | 1828 | # Renamed 1829 | GT_TEST = GT_TEST_AI_VS_AI 1830 | 1831 | # Removed 1832 | # GT_TOURNAMENT = 23 1833 | # GT_RESERVED_18_22 = 26 1834 | # GT_RESERVED_18_23 = 27 1835 | 1836 | def as_bnet(self, format: FormatType = FormatType.FT_STANDARD): 1837 | if self == GameType.GT_RANKED: 1838 | if format == FormatType.FT_WILD: 1839 | return BnetGameType.BGT_RANKED_WILD 1840 | elif format == FormatType.FT_STANDARD: 1841 | return BnetGameType.BGT_RANKED_STANDARD 1842 | elif format == FormatType.FT_CLASSIC: 1843 | return BnetGameType.BGT_RANKED_CLASSIC 1844 | elif format == FormatType.FT_TWIST: 1845 | return BnetGameType.BGT_RANKED_TWIST 1846 | else: 1847 | raise ValueError() 1848 | if self == GameType.GT_CASUAL: 1849 | if format == FormatType.FT_WILD: 1850 | return BnetGameType.BGT_CASUAL_WILD 1851 | elif format == FormatType.FT_STANDARD: 1852 | return BnetGameType.BGT_CASUAL_STANDARD 1853 | elif format == FormatType.FT_CLASSIC: 1854 | return BnetGameType.BGT_CASUAL_CLASSIC 1855 | elif format == FormatType.FT_TWIST: 1856 | return BnetGameType.BGT_CASUAL_TWIST 1857 | else: 1858 | raise ValueError() 1859 | 1860 | return { 1861 | GameType.GT_UNKNOWN: BnetGameType.BGT_UNKNOWN, 1862 | GameType.GT_VS_AI: BnetGameType.BGT_VS_AI, 1863 | GameType.GT_VS_FRIEND: BnetGameType.BGT_FRIENDS, 1864 | GameType.GT_TUTORIAL: BnetGameType.BGT_TUTORIAL, 1865 | GameType.GT_ARENA: BnetGameType.BGT_ARENA, 1866 | GameType.GT_TEST_AI_VS_AI: BnetGameType.BGT_TEST1, 1867 | GameType.GT_TAVERNBRAWL: BnetGameType.BGT_TAVERNBRAWL_PVP, 1868 | GameType.GT_TB_1P_VS_AI: BnetGameType.BGT_TAVERNBRAWL_1P_VERSUS_AI, 1869 | GameType.GT_TB_2P_COOP: BnetGameType.BGT_TAVERNBRAWL_2P_COOP, 1870 | GameType.GT_FSG_BRAWL_VS_FRIEND: BnetGameType.BGT_FSG_BRAWL_VS_FRIEND, 1871 | GameType.GT_FSG_BRAWL: BnetGameType.BGT_FSG_BRAWL_PVP, 1872 | GameType.GT_FSG_BRAWL_1P_VS_AI: BnetGameType.BGT_FSG_BRAWL_1P_VERSUS_AI, 1873 | GameType.GT_FSG_BRAWL_2P_COOP: BnetGameType.BGT_FSG_BRAWL_2P_COOP, 1874 | GameType.GT_BATTLEGROUNDS: BnetGameType.BGT_BATTLEGROUNDS, 1875 | GameType.GT_BATTLEGROUNDS_FRIENDLY: BnetGameType.BGT_BATTLEGROUNDS_FRIENDLY, 1876 | GameType.GT_PVPDR_PAID: BnetGameType.BGT_PVPDR_PAID, 1877 | GameType.GT_PVPDR: BnetGameType.BGT_PVPDR, 1878 | GameType.GT_MERCENARIES_PVP: BnetGameType.BGT_MERCENARIES_PVP, 1879 | GameType.GT_MERCENARIES_PVE: BnetGameType.BGT_MERCENARIES_PVE, 1880 | GameType.GT_MERCENARIES_PVE_COOP: BnetGameType.BGT_MERCENARIES_PVE_COOP, 1881 | GameType.GT_MERCENARIES_FRIENDLY: BnetGameType.BGT_MERCENARIES_FRIENDLY, 1882 | GameType.GT_BATTLEGROUNDS_DUO: BnetGameType.BGT_BATTLEGROUNDS_DUO, 1883 | GameType.GT_BATTLEGROUNDS_DUO_VS_AI: BnetGameType.BGT_BATTLEGROUNDS_DUO_VS_AI, 1884 | GameType.GT_BATTLEGROUNDS_DUO_FRIENDLY: BnetGameType.BGT_BATTLEGROUNDS_DUO_FRIENDLY, 1885 | GameType.GT_UNDERGROUND_ARENA: BnetGameType.BGT_UNDERGROUND_ARENA 1886 | }[self] 1887 | 1888 | @property 1889 | def is_fireside(self): 1890 | return self.name.startswith("GT_FSG_") 1891 | 1892 | @property 1893 | def is_tavern_brawl(self): 1894 | return self.name in ("GT_TAVERNBRAWL", "GT_TB_1P_VS_AI", "GT_TB_2P_COOP") 1895 | 1896 | 1897 | class BnetRegion(IntEnum): 1898 | """Blizzard.GameService.SDK.Client.Integration/BnetRegion""" 1899 | 1900 | REGION_UNINITIALIZED = -1, 1901 | REGION_UNKNOWN = 0 1902 | REGION_US = 1 1903 | REGION_EU = 2 1904 | REGION_KR = 3 1905 | REGION_TW = 4 1906 | REGION_CN = 5 1907 | REGION_LIVE_VERIFICATION = 40 1908 | REGION_PTR_LOC = 41 1909 | 1910 | # Deleted 1911 | REGION_MSCHWEITZER_BN11 = 52 1912 | REGION_MSCHWEITZER_BN12 = 53 1913 | REGION_DEV = 60 1914 | REGION_PTR = 98 1915 | 1916 | @classmethod 1917 | def from_account_hi(cls, hi): 1918 | # AI: 0x200000000000000 (144115188075855872) 1919 | # US: 0x200000157544347 (144115193835963207) 1920 | # EU: 0x200000257544347 (144115198130930503) 1921 | # KR: 0x200000357544347 (144115202425897799) (TW on same region) 1922 | # CN: 0x200000557544347 (144115211015832391) 1923 | # XX: 0x200000069506164 (144115189842731364) (Tutorial) 1924 | return cls((hi >> 32) & 0xFF) 1925 | 1926 | @property 1927 | def is_live(self): 1928 | return self.name in ( 1929 | "REGION_US", 1930 | "REGION_EU", 1931 | "REGION_KR", 1932 | "REGION_CN", 1933 | ) 1934 | 1935 | 1936 | # Deleted 1937 | 1938 | class GoldRewardState(IntEnum): 1939 | """TAG_GOLD_REWARD_STATE""" 1940 | 1941 | INVALID = 0 1942 | ELIGIBLE = 1 1943 | WRONG_GAME_TYPE = 2 1944 | ALREADY_CAPPED = 3 1945 | BAD_RATING = 4 1946 | SHORT_GAME_BY_TIME = 5 1947 | OVER_CAIS = 6 1948 | 1949 | # Renamed 1950 | SHORT_GAME = SHORT_GAME_BY_TIME 1951 | 1952 | 1953 | class MetaDataType(IntEnum): 1954 | """PegasusGame.HistoryMeta.Type""" 1955 | 1956 | TARGET = 0 1957 | DAMAGE = 1 1958 | HEALING = 2 1959 | JOUST = 3 1960 | SHOW_BIG_CARD = 5 1961 | EFFECT_TIMING = 6 1962 | HISTORY_TARGET = 7 1963 | OVERRIDE_HISTORY = 8 1964 | HISTORY_TARGET_DONT_DUPLICATE_UNTIL_END = 9 1965 | BEGIN_ARTIFICIAL_HISTORY_TILE = 10 1966 | BEGIN_ARTIFICIAL_HISTORY_TRIGGER_TILE = 11 1967 | END_ARTIFICIAL_HISTORY_TILE = 12 1968 | START_DRAW = 13 1969 | BURNED_CARD = 14 1970 | EFFECT_SELECTION = 15 1971 | BEGIN_LISTENING_FOR_TURN_EVENTS = 16 1972 | HOLD_DRAWN_CARD = 17 1973 | CONTROLLER_AND_ZONE_CHANGE = 18 1974 | ARTIFICIAL_PAUSE = 19 1975 | SLUSH_TIME = 20 1976 | ARTIFICIAL_HISTORY_INTERRUPT = 21 1977 | POISONOUS = 22 1978 | CRITICAL_HIT = 23 1979 | HISTORY_TRIGGER_SOURCE = 24 1980 | HISTORY_SOURCE_OWNER = 25 1981 | HISTORY_REMOVE_ENTITIES = 26 1982 | SPEND_HEALTH = 27 1983 | SPEND_ARMOR = 28 1984 | 1985 | # Renamed in 9786 from PowerHistoryMetaData.Type 1986 | META_TARGET = TARGET 1987 | META_DAMAGE = DAMAGE 1988 | META_HEALING = HEALING 1989 | 1990 | # Renamed in 30795 1991 | ARTIFICIAL_PAUSE_STUBBED_FOR_14_2 = ARTIFICIAL_PAUSE 1992 | 1993 | # Renamed in 93227 1994 | STUB_20_6_LETTUCE = CRITICAL_HIT 1995 | 1996 | # Deleted 1997 | CLIENT_HISTORY = 4 1998 | 1999 | 2000 | class Mulligan(IntEnum): 2001 | """TAG_MULLIGAN""" 2002 | 2003 | INVALID = 0 2004 | INPUT = 1 2005 | DEALING = 2 2006 | WAITING = 3 2007 | DONE = 4 2008 | 2009 | 2010 | # Deleted 2011 | 2012 | class MultiClassGroup(IntEnum): 2013 | """TAG_MULTI_CLASS_GROUP""" 2014 | 2015 | INVALID = 0 2016 | GRIMY_GOONS = 1 2017 | JADE_LOTUS = 2 2018 | KABAL = 3 2019 | 2020 | # The values below are synthesized from the card classes metadata in the client 2021 | 2022 | PALADIN_PRIEST = 4 2023 | PRIEST_WARLOCK = 5 2024 | WARLOCK_DEMONHUNTER = 6 2025 | HUNTER_DEMONHUNTER = 7 2026 | DRUID_HUNTER = 8 2027 | DRUID_SHAMAN = 9 2028 | MAGE_SHAMAN = 10 2029 | MAGE_ROGUE = 11 2030 | ROGUE_WARRIOR = 12 2031 | PALADIN_WARRIOR = 13 2032 | 2033 | MAGE_HUNTER = 28 2034 | HUNTER_DEATHKNIGHT = 29 2035 | DEATHKNIGHT_PALADIN = 30 2036 | PALADIN_SHAMAN = 31 2037 | SHAMAN_WARRIOR = 32 2038 | WARRIOR_DEMONHUNTER = 33 2039 | DEMONHUNTER_ROGUE = 34 2040 | ROGUE_PRIEST = 35 2041 | PRIEST_DRUID = 36 2042 | DRUID_WARLOCK = 37 2043 | WARLOCK_MAGE = 38 2044 | 2045 | @property 2046 | def card_classes(self): 2047 | # Gadgetzan 2048 | if self == MultiClassGroup.GRIMY_GOONS: 2049 | return [CardClass.HUNTER, CardClass.WARRIOR, CardClass.PALADIN] 2050 | elif self == MultiClassGroup.JADE_LOTUS: 2051 | return [CardClass.ROGUE, CardClass.SHAMAN, CardClass.DRUID] 2052 | elif self == MultiClassGroup.KABAL: 2053 | return [CardClass.PRIEST, CardClass.WARLOCK, CardClass.MAGE] 2054 | 2055 | # Scholomance 2056 | if self == MultiClassGroup.PALADIN_PRIEST: 2057 | return [CardClass.PALADIN, CardClass.PRIEST] 2058 | elif self == MultiClassGroup.PRIEST_WARLOCK: 2059 | return [CardClass.PRIEST, CardClass.WARLOCK] 2060 | elif self == MultiClassGroup.WARLOCK_DEMONHUNTER: 2061 | return [CardClass.WARLOCK, CardClass.DEMONHUNTER] 2062 | elif self == MultiClassGroup.HUNTER_DEMONHUNTER: 2063 | return [CardClass.HUNTER, CardClass.DEMONHUNTER] 2064 | elif self == MultiClassGroup.DRUID_HUNTER: 2065 | return [CardClass.DRUID, CardClass.HUNTER] 2066 | elif self == MultiClassGroup.DRUID_SHAMAN: 2067 | return [CardClass.DRUID, CardClass.SHAMAN] 2068 | elif self == MultiClassGroup.MAGE_SHAMAN: 2069 | return [CardClass.MAGE, CardClass.SHAMAN] 2070 | elif self == MultiClassGroup.MAGE_ROGUE: 2071 | return [CardClass.MAGE, CardClass.ROGUE] 2072 | elif self == MultiClassGroup.ROGUE_WARRIOR: 2073 | return [CardClass.ROGUE, CardClass.WARRIOR] 2074 | elif self == MultiClassGroup.PALADIN_WARRIOR: 2075 | return [CardClass.PALADIN, CardClass.WARRIOR] 2076 | 2077 | # Audiopocalypse 2078 | if self == MultiClassGroup.MAGE_HUNTER: 2079 | return [CardClass.MAGE, CardClass.HUNTER] 2080 | elif self == MultiClassGroup.HUNTER_DEATHKNIGHT: 2081 | return [CardClass.HUNTER, CardClass.DEATHKNIGHT] 2082 | elif self == MultiClassGroup.DEATHKNIGHT_PALADIN: 2083 | return [CardClass.DEATHKNIGHT, CardClass.PALADIN] 2084 | elif self == MultiClassGroup.PALADIN_SHAMAN: 2085 | return [CardClass.PALADIN, CardClass.SHAMAN] 2086 | elif self == MultiClassGroup.SHAMAN_WARRIOR: 2087 | return [CardClass.SHAMAN, CardClass.WARRIOR] 2088 | elif self == MultiClassGroup.WARRIOR_DEMONHUNTER: 2089 | return [CardClass.WARRIOR, CardClass.DEMONHUNTER] 2090 | elif self == MultiClassGroup.DEMONHUNTER_ROGUE: 2091 | return [CardClass.DEMONHUNTER, CardClass.ROGUE] 2092 | elif self == MultiClassGroup.ROGUE_PRIEST: 2093 | return [CardClass.ROGUE, CardClass.PRIEST] 2094 | elif self == MultiClassGroup.PRIEST_DRUID: 2095 | return [CardClass.PRIEST, CardClass.DRUID] 2096 | elif self == MultiClassGroup.DRUID_WARLOCK: 2097 | return [CardClass.DRUID, CardClass.WARLOCK] 2098 | elif self == MultiClassGroup.WARLOCK_MAGE: 2099 | return [CardClass.WARLOCK, CardClass.MAGE] 2100 | 2101 | return [] 2102 | 2103 | 2104 | class FactionColorType(IntEnum): 2105 | """CardColorSwitcher.FactionColorType""" 2106 | 2107 | GENERIC = 0 2108 | GRIMY_GOONS = 1 2109 | KABAL = 2 2110 | JADE_LOTUS = 3 2111 | ZERG = 4 2112 | TERRAN = 5 2113 | PROTOSS = 6 2114 | 2115 | 2116 | class SpellSchool(IntEnum): 2117 | """TAG_SPELL_SCHOOL""" 2118 | 2119 | NONE = 0 2120 | ARCANE = 1 2121 | FIRE = 2 2122 | FROST = 3 2123 | NATURE = 4 2124 | HOLY = 5 2125 | SHADOW = 6 2126 | FEL = 7 2127 | PHYSICAL_COMBAT = 8 2128 | TAVERN = 9 2129 | SPELLCRAFT = 10 2130 | LESSER_TRINKET = 11 2131 | GREATER_TRINKET = 12 2132 | UPGRADE = 13 2133 | 2134 | 2135 | class OptionType(IntEnum): 2136 | """PegasusGame.Option.Type""" 2137 | 2138 | PASS = 1 2139 | END_TURN = 2 2140 | POWER = 3 2141 | 2142 | 2143 | class PlayState(IntEnum): 2144 | """TAG_PLAYSTATE""" 2145 | 2146 | INVALID = 0 2147 | PLAYING = 1 2148 | WINNING = 2 2149 | LOSING = 3 2150 | WON = 4 2151 | LOST = 5 2152 | TIED = 6 2153 | DISCONNECTED = 7 2154 | CONCEDED = 8 2155 | 2156 | # Renamed in 10833 2157 | QUIT = CONCEDED 2158 | 2159 | 2160 | class PowerType(IntEnum): 2161 | """Network.PowerType""" 2162 | 2163 | FULL_ENTITY = 1 2164 | SHOW_ENTITY = 2 2165 | HIDE_ENTITY = 3 2166 | TAG_CHANGE = 4 2167 | BLOCK_START = 5 2168 | BLOCK_END = 6 2169 | CREATE_GAME = 7 2170 | META_DATA = 8 2171 | CHANGE_ENTITY = 9 2172 | RESET_GAME = 10 2173 | SUB_SPELL_START = 11 2174 | SUB_SPELL_END = 12 2175 | VO_SPELL = 13 2176 | CACHED_TAG_FOR_DORMANT_CHANGE = 14 2177 | SHUFFLE_DECK = 15 2178 | VO_BANTER = 16 2179 | 2180 | # Renamed in 12574 2181 | ACTION_START = BLOCK_START 2182 | ACTION_END = BLOCK_END 2183 | 2184 | 2185 | class BlockType(IntEnum): 2186 | """PegasusGame.HistoryBlock.Type""" 2187 | 2188 | INVALID = 0 2189 | ATTACK = 1 2190 | JOUST = 2 2191 | POWER = 3 2192 | TRIGGER = 5 2193 | DEATHS = 6 2194 | PLAY = 7 2195 | FATIGUE = 8 2196 | REVEAL_CARD = 10 2197 | GAME_RESET = 11 2198 | MOVE_MINION = 12 2199 | DECK_ACTION = 13 2200 | 2201 | # Removed 2202 | SCRIPT = 4 2203 | RITUAL = 9 2204 | ACTION = 99 2205 | 2206 | # Renamed 2207 | CONTINUOUS = 2 2208 | TRADE = DECK_ACTION 2209 | 2210 | 2211 | class State(IntEnum): 2212 | """TAG_STATE""" 2213 | 2214 | INVALID = 0 2215 | LOADING = 1 2216 | RUNNING = 2 2217 | COMPLETE = 3 2218 | 2219 | 2220 | class Step(IntEnum): 2221 | """TAG_STEP""" 2222 | 2223 | INVALID = 0 2224 | BEGIN_FIRST = 1 2225 | BEGIN_SHUFFLE = 2 2226 | BEGIN_DRAW = 3 2227 | BEGIN_MULLIGAN = 4 2228 | MAIN_BEGIN = 5 2229 | MAIN_READY = 6 2230 | MAIN_RESOURCE = 7 2231 | MAIN_DRAW = 8 2232 | MAIN_START = 9 2233 | MAIN_ACTION = 10 2234 | MAIN_COMBAT = 11 2235 | MAIN_END = 12 2236 | MAIN_NEXT = 13 2237 | FINAL_WRAPUP = 14 2238 | FINAL_GAMEOVER = 15 2239 | MAIN_CLEANUP = 16 2240 | MAIN_START_TRIGGERS = 17 2241 | MAIN_SET_ACTION_STEP_TYPE = 18 2242 | MAIN_PRE_ACTION = 19 2243 | MAIN_POST_ACTION = 20 2244 | 2245 | 2246 | ## 2247 | # Misc 2248 | 2249 | class Booster(IntEnum): 2250 | """BoosterDbId""" 2251 | 2252 | INVALID = 0 2253 | CLASSIC = 1 2254 | GOBLINS_VS_GNOMES = 9 2255 | THE_GRAND_TOURNAMENT = 10 2256 | OLD_GODS = 11 2257 | # Replaced - see below 2258 | # FIRST_PURCHASE = 17 2259 | FIRST_PURCHASE_OLD = 17 2260 | SIGNUP_INCENTIVE = 18 2261 | MEAN_STREETS = 19 2262 | UNGORO = 20 2263 | FROZEN_THRONE = 21 2264 | GOLDEN_CLASSIC_PACK = 23 2265 | KOBOLDS_AND_CATACOMBS = 30 2266 | WITCHWOOD = 31 2267 | THE_BOOMSDAY_PROJECT = 38 2268 | RASTAKHANS_RUMBLE = 40 2269 | MAMMOTH_BUNDLE = 41 2270 | DALARAN = 49 2271 | FIRST_PURCHASE = 181 2272 | ULDUM = 128 2273 | DRAGONS = 347 2274 | BLACK_TEMPLE = 423 2275 | SCHOLOMANCE = 468 2276 | STANDARD_HUNTER = 470 2277 | YEAR_OF_DRAGON = 498 2278 | STANDARD_MAGE = 545 2279 | THE_BARRENS = 553 2280 | STORMWIND = 602 2281 | GOLDEN_SCHOLOMANCE = 603 2282 | DARKMOON_FAIRE = 616 2283 | MERCENARIES = 629 2284 | STANDARD_DRUID = 631 2285 | STANDARD_PALADIN = 632 2286 | STANDARD_WARRIOR = 633 2287 | STANDARD_PRIEST = 634 2288 | STANDARD_ROGUE = 635 2289 | STANDARD_SHAMAN = 636 2290 | STANDARD_WARLOCK = 637 2291 | STANDARD_DEMONHUNTER = 638 2292 | GOLDEN_DARKMOON_FAIRE = 643 2293 | ALTERAC_VALLEY = 665 2294 | GOLDEN_THE_BARRENS = 686 2295 | YEAR_OF_THE_PHOENIX = 688 2296 | THE_SUNKEN_CITY = 694 2297 | STANDARD_PACK = 713 2298 | WILD_PACK = 714 2299 | GOLDEN_STANDARD_PACK = 716 2300 | REVENDRETH = 729 2301 | STORMWIND_GOLDEN = 737 2302 | TITANS = 819 2303 | RETURN_OF_THE_LICH_KING = 821 2304 | ALTERAC_VALLEY_GOLDEN = 841 2305 | BATTLE_OF_THE_BANDS = 854 2306 | CAVERNS_OF_TIME = 894 2307 | PATH_OF_ARTHAS = 903 2308 | WILD_WEST = 922 2309 | 2310 | # Renamed 2311 | KOBOLDS_CATACOMBS = KOBOLDS_AND_CATACOMBS 2312 | 2313 | # Deleted 2314 | WAILING_CAVERNS = 583 2315 | 2316 | 2317 | class BrawlType(IntEnum): 2318 | """PegasusShared.BrawlType""" 2319 | 2320 | BRAWL_TYPE_UNKNOWN = 0 2321 | BRAWL_TYPE_TAVERN_BRAWL = 1 2322 | BRAWL_TYPE_FIRESIDE_GATHERING = 2 2323 | BRAWL_TYPE_COUNT = 3 2324 | # BRAWL_TYPE_FIRST = 1 2325 | 2326 | 2327 | class CardTextBuilderType(IntEnum): 2328 | """Assets.Card.CardTextBuilderType""" 2329 | 2330 | DEFAULT = 0 2331 | JADE_GOLEM = 1 2332 | JADE_GOLEM_TRIGGER = 2 2333 | MODULAR_ENTITY = 3 2334 | KAZAKUS_POTION_EFFECT = 4 2335 | PRIMORDIAL_WAND = 5 2336 | ALTERNATE_CARD_TEXT = 6 2337 | SCRIPT_DATA_NUM_1 = 7 2338 | GALAKROND_COUNTER = 8 2339 | DECORATE = 9 2340 | PLAYER_TAG_THRESHOLD = 10 2341 | ENTITY_TAG_THRESHOLD = 11 2342 | MULTIPLE_ENTITY_NAMES = 12 2343 | GAMEPLAY_STRING = 13 2344 | ZOMBEAST = 14 2345 | ZOMBEAST_ENCHANTMENT = 15 2346 | HIDDEN_CHOICE = 16 2347 | INVESTIGATE = 17 2348 | REFERENCE_CREATOR_ENTITY = 18 2349 | REFERENCE_SCRIPT_DATA_NUM_1_ENTITY = 19 2350 | REFERENCE_SCRIPT_DATA_NUM_1_NUM_2_ENTITY = 20 2351 | UNDATAKAH_ENCHANT = 21 2352 | SPELL_DAMAGE_ONLY = 22 2353 | DRUSTVAR_HORROR = 23 2354 | HIDDEN_ENTITY = 24 2355 | SCORE_VALUE_COUNT_DOWN = 25 2356 | SCRIPT_DATA_NUM_1_NUM_2 = 26 2357 | POWERED_UP = 27 2358 | MULTIPLE_ALT_TEXT_SCRIPT_DATA_NUMS = 28 2359 | REFERENCE_SCRIPT_DATA_NUM_1_ENTITY_POWER = 29 2360 | REFERENCE_SCRIPT_DATA_NUM_1_CARD_DBID = 30 2361 | REFERENCE_SCRIPT_DATA_NUM_CARD_RACE = 31 2362 | BG_QUEST = 32 2363 | MULTIPLE_ALT_TEXT_SCRIPT_DATA_NUMS_REF_SDN6_CARD_DBID = 33 2364 | ZILLIAX_DELUXE_3000 = 34 2365 | 2366 | # Renamed 2367 | DEPRECATED_5 = PRIMORDIAL_WAND 2368 | DEPRECATED_6 = ALTERNATE_CARD_TEXT 2369 | DEPRECATED_8 = GALAKROND_COUNTER 2370 | DEPRECATED_10 = PLAYER_TAG_THRESHOLD 2371 | DEPRECATED_11 = ENTITY_TAG_THRESHOLD 2372 | DEPRECATED_12 = MULTIPLE_ENTITY_NAMES 2373 | KAZAKUS_POTION = MODULAR_ENTITY 2374 | PLACEHOLDER_01 = REFERENCE_SCRIPT_DATA_NUM_1_NUM_2_ENTITY 2375 | PLACE_HOLDER_02 = UNDATAKAH_ENCHANT 2376 | PLACE_HOLDER_7 = SCRIPT_DATA_NUM_1 2377 | PLACE_HOLDER_8 = GALAKROND_COUNTER 2378 | PLACE_HOLDER_10 = DEPRECATED_10 2379 | PLACE_HOLDER_11 = DEPRECATED_11 2380 | PLACE_HOLDER_12 = DEPRECATED_12 2381 | PLACE_HOLDER_13 = GAMEPLAY_STRING 2382 | PLACE_HOLDER_17 = INVESTIGATE 2383 | 2384 | 2385 | class DeckType(IntEnum): 2386 | """PegasusShared.DeckType""" 2387 | 2388 | CLIENT_ONLY_DECK = -1 2389 | UNKNOWN_DECK_TYPE = 0 2390 | NORMAL_DECK = 1 2391 | AI_DECK = 2 2392 | DRAFT_DECK = 4 2393 | PRECON_DECK = 5 2394 | TAVERN_BRAWL_DECK = 6 2395 | FSG_BRAWL_DECK = 7 2396 | PVPDR_DECK = 8 2397 | PVPDR_DISPLAY_DECK = 9 2398 | HIDDEN_DECK = 1000 2399 | 2400 | # Removed 2401 | # FRIENDLY_TOURNAMENT_DECK = 8 2402 | 2403 | 2404 | class DraftSlotType(IntEnum): 2405 | """PegasusShared.DraftSlotType""" 2406 | 2407 | DRAFT_SLOT_NONE = 0 2408 | DRAFT_SLOT_CARD = 1 2409 | DRAFT_SLOT_HERO = 2 2410 | DRAFT_SLOT_HERO_POWER = 3 2411 | 2412 | 2413 | class DungeonRewardOption(IntEnum): 2414 | """AdventureDungeonCrawlPlayMat.OptionType""" 2415 | 2416 | INVALID = 0 2417 | LOOT = 1 2418 | TREASURE = 2 2419 | SHRINE_TREASURE = 3 2420 | HERO_POWER = 4 2421 | DECK = 5 2422 | 2423 | 2424 | class TavernBrawlMode(IntEnum): 2425 | """PegasusShared.TavernBrawlMode""" 2426 | 2427 | TB_MODE_NORMAL = 0 2428 | TB_MODE_HEROIC = 1 2429 | 2430 | 2431 | class RewardType(IntEnum): 2432 | """Reward.Type""" 2433 | 2434 | NONE = -1 2435 | ARCANE_DUST = 0 2436 | BOOSTER_PACK = 1 2437 | CARD = 2 2438 | CARD_BACK = 3 2439 | CRAFTABLE_CARD = 4 2440 | FORGE_TICKET = 5 2441 | GOLD = 6 2442 | MOUNT = 7 2443 | CLASS_CHALLENGE = 8 2444 | EVENT = 9 2445 | RANDOM_CARD = 10 2446 | BONUS_CHALLENGE = 11 2447 | ADVENTURE_DECK = 12 2448 | ADVENTURE_HERO_POWER = 13 2449 | ARCANE_ORBS = 14 2450 | DECK = 15 2451 | MINI_SET = 16 2452 | MERCENARY_COIN = 17 2453 | MERCENARY_EXP = 18 2454 | MERCENARY_ABILITY_UNLOCK = 19 2455 | MERCENARY_EQUIPMENT = 20 2456 | REWARD_ITEM = 21 2457 | MERCENARY_BOOSTER = 22 2458 | MERCENARY_MERCENARY = 23 2459 | MERCENARY_RANDOM_MERCENARY = 24 2460 | MERCENARY_KNOCKOUT = 25 2461 | BATTLEGROUNDS_GUIDE_SKIN = 26 2462 | BATTLEGROUNDS_HERO_SKIN = 27 2463 | BATTLEGROUNDS_FINISHER = 28 2464 | BATTLEGROUNDS_BOARD_SKIN = 29 2465 | BATTLEGROUNDS_EMOTE = 30 2466 | MERCENARY_RENOWN = 31 2467 | 2468 | 2469 | # Deleted 2470 | 2471 | class SwissDeckType(IntEnum): 2472 | """PegasusUtilTournament.SwissDeckType""" 2473 | 2474 | SWISS_DECK_NONE = 0 2475 | SWISS_DECK_CONQUEST = 1 2476 | SWISS_DECK_LAST_STAND = 2 2477 | 2478 | 2479 | # Deleted 2480 | 2481 | class TournamentState(IntEnum): 2482 | """PegasusUtilTournament.TournamentState""" 2483 | 2484 | STATE_OPEN = 1 2485 | STATE_LOCKED = 2 2486 | STATE_STARTED = 3 2487 | STATE_CLOSED = 4 2488 | 2489 | 2490 | # Deleted 2491 | 2492 | class TournamentType(IntEnum): 2493 | """PegasusUtilTournament.TournamentType""" 2494 | 2495 | TYPE_UNKNOWN = 0 2496 | TYPE_SWISS = 1 2497 | 2498 | 2499 | class Type(IntEnum): 2500 | """TAG_TYPE""" 2501 | 2502 | UNKNOWN = 0 2503 | BOOL = 1 2504 | NUMBER = 2 2505 | COUNTER = 3 2506 | ENTITY = 4 2507 | PLAYER = 5 2508 | TEAM = 6 2509 | ENTITY_DEFINITION = 7 2510 | STRING = 8 2511 | 2512 | # Not present at the time 2513 | LOCSTRING = -2 2514 | 2515 | 2516 | TAG_TYPES = { 2517 | GameTag.TRIGGER_VISUAL: Type.BOOL, 2518 | GameTag.ELITE: Type.BOOL, 2519 | GameTag.CARD_SET: CardSet, 2520 | GameTag.CARDTEXT_INHAND: Type.LOCSTRING, 2521 | GameTag.CARDNAME: Type.LOCSTRING, 2522 | GameTag.WINDFURY: Type.BOOL, 2523 | GameTag.TAUNT: Type.BOOL, 2524 | GameTag.STEALTH: Type.BOOL, 2525 | GameTag.SPELLPOWER: Type.BOOL, 2526 | GameTag.DIVINE_SHIELD: Type.BOOL, 2527 | GameTag.CHARGE: Type.BOOL, 2528 | GameTag.CLASS: CardClass, 2529 | GameTag.CARDRACE: Race, 2530 | GameTag.FACTION: Faction, 2531 | GameTag.RARITY: Rarity, 2532 | GameTag.CARDTYPE: CardType, 2533 | GameTag.FREEZE: Type.BOOL, 2534 | GameTag.ENRAGED: Type.BOOL, 2535 | GameTag.DEATHRATTLE: Type.BOOL, 2536 | GameTag.BATTLECRY: Type.BOOL, 2537 | GameTag.SECRET: Type.BOOL, 2538 | GameTag.COMBO: Type.BOOL, 2539 | GameTag.IMMUNE: Type.BOOL, 2540 | # GameTag.AttackVisualType: AttackVisualType, 2541 | GameTag.CardTextInPlay: Type.LOCSTRING, 2542 | # GameTag.DevState: DevState, 2543 | GameTag.MORPH: Type.BOOL, 2544 | GameTag.COLLECTIBLE: Type.BOOL, 2545 | GameTag.TARGETING_ARROW_TEXT: Type.LOCSTRING, 2546 | GameTag.ENCHANTMENT_BIRTH_VISUAL: EnchantmentVisual, 2547 | GameTag.ENCHANTMENT_IDLE_VISUAL: EnchantmentVisual, 2548 | GameTag.InvisibleDeathrattle: Type.BOOL, 2549 | GameTag.TAG_ONE_TURN_EFFECT: Type.BOOL, 2550 | GameTag.SILENCE: Type.BOOL, 2551 | GameTag.COUNTER: Type.BOOL, 2552 | GameTag.ARTISTNAME: Type.STRING, 2553 | GameTag.LocalizationNotes: Type.STRING, 2554 | GameTag.ImmuneToSpellpower: Type.BOOL, 2555 | GameTag.ADJACENT_BUFF: Type.BOOL, 2556 | GameTag.FLAVORTEXT: Type.LOCSTRING, 2557 | GameTag.HealTarget: Type.BOOL, 2558 | GameTag.AURA: Type.BOOL, 2559 | GameTag.POISONOUS: Type.BOOL, 2560 | GameTag.HOW_TO_EARN: Type.LOCSTRING, 2561 | GameTag.HOW_TO_EARN_GOLDEN: Type.LOCSTRING, 2562 | GameTag.AI_MUST_PLAY: Type.BOOL, 2563 | GameTag.AFFECTED_BY_SPELL_POWER: Type.BOOL, 2564 | GameTag.SPARE_PART: Type.BOOL, 2565 | GameTag.PLAYSTATE: PlayState, 2566 | GameTag.ZONE: Zone, 2567 | GameTag.FAKE_ZONE: Zone, 2568 | GameTag.STEP: Step, 2569 | GameTag.NEXT_STEP: Step, 2570 | GameTag.STATE: State, 2571 | GameTag.MULLIGAN_STATE: Mulligan, 2572 | GameTag.AUTO_ATTACK: Type.BOOL, 2573 | GameTag.SPELL_SCHOOL: SpellSchool, 2574 | GameTag.LETTUCE_ROLE: Role, 2575 | } 2576 | 2577 | 2578 | LOCALIZED_TAGS = [k for k, v in TAG_TYPES.items() if v == Type.LOCSTRING] 2579 | 2580 | 2581 | class PuzzleType(IntEnum): 2582 | """TAG_PUZZLE_TYPE""" 2583 | 2584 | INVALID = 0 2585 | MIRROR = 1 2586 | LETHAL = 2 2587 | SURVIVAL = 3 2588 | CLEAR = 4 2589 | 2590 | 2591 | class Locale(IntEnum): 2592 | """Locale""" 2593 | 2594 | UNKNOWN = -1 2595 | enUS = 0 2596 | enGB = 1 2597 | frFR = 2 2598 | deDE = 3 2599 | koKR = 4 2600 | esES = 5 2601 | esMX = 6 2602 | ruRU = 7 2603 | zhTW = 8 2604 | zhCN = 9 2605 | itIT = 10 2606 | ptBR = 11 2607 | plPL = 12 2608 | ptPT = 13 2609 | jaJP = 14 2610 | thTH = 15 2611 | 2612 | @property 2613 | def unused(self): 2614 | return self.name in ("UNKNOWN", "enGB", "ptPT") 2615 | 2616 | @property 2617 | def name_global(self): 2618 | if self.name == "enGB": 2619 | return "GLOBAL_LANGUAGE_NATIVE_ENUS" 2620 | return "GLOBAL_LANGUAGE_NATIVE_%s" % (self.name.upper()) 2621 | 2622 | 2623 | def get_localized_name(v, locale="enUS"): 2624 | name_global = getattr(v, "name_global", "") 2625 | if not name_global: 2626 | return "" 2627 | 2628 | from .stringsfile import load_globalstrings 2629 | 2630 | globalstrings = load_globalstrings(locale) 2631 | return globalstrings.get(name_global, {}).get("TEXT", "") 2632 | 2633 | 2634 | class ZodiacYear(IntEnum): 2635 | INVALID = -1 2636 | PRE_STANDARD = 0 2637 | KRAKEN = 1 2638 | MAMMOTH = 2 2639 | RAVEN = 3 2640 | DRAGON = 4 2641 | PHOENIX = 5 2642 | GRYPHON = 6 2643 | HYDRA = 7 2644 | WOLF = 8 2645 | PEGASUS = 9 2646 | 2647 | @property 2648 | def standard_card_sets(self): 2649 | from .utils import STANDARD_SETS 2650 | return STANDARD_SETS.get(self, []) 2651 | 2652 | @classmethod 2653 | def as_of_date(self, date=None): 2654 | from .utils import ZODIAC_ROTATION_DATES 2655 | 2656 | if date is None: 2657 | date = datetime.now() 2658 | 2659 | ret = ZodiacYear.INVALID 2660 | rotation_dates = sorted(ZODIAC_ROTATION_DATES.items(), key=lambda x: x[1]) 2661 | for enum_value, rotation_date in rotation_dates: 2662 | if rotation_date > date: 2663 | break 2664 | ret = enum_value 2665 | 2666 | return ret 2667 | 2668 | 2669 | if __name__ == "__main__": 2670 | import json 2671 | import sys 2672 | from collections import OrderedDict 2673 | 2674 | def get_enum_key(enum, name): 2675 | val = enum[name].value 2676 | canonical_name = enum[name].name 2677 | if canonical_name == name: 2678 | return -100_000 + val 2679 | return val 2680 | 2681 | # Consistently sort, but keep aliases at the end 2682 | all_enums = OrderedDict(sorted( 2683 | [ 2684 | ( 2685 | k, OrderedDict(sorted(v.__members__.items(), key=lambda x: get_enum_key(v, x[0]))) 2686 | ) for k, v in globals().items() 2687 | if isinstance(v, type) and issubclass(v, IntEnum) and k != "IntEnum" 2688 | ], 2689 | key=lambda x: x[0] 2690 | )) 2691 | 2692 | def _print_enums(enums, format): 2693 | ret = [] 2694 | linefmt = "\t%s = %i," 2695 | for enum in enums: 2696 | lines = "\n".join(linefmt % (name, value) for name, value in enums[enum].items()) 2697 | ret.append(format % (enum, lines)) 2698 | print("\n\n".join(ret)) 2699 | 2700 | if len(sys.argv) >= 2: 2701 | format = sys.argv[1] 2702 | else: 2703 | format = "--json" 2704 | 2705 | if format == "--ts": 2706 | _print_enums(all_enums, "export const enum %s {\n%s\n}") 2707 | elif format == "--cs": 2708 | _print_enums(all_enums, "public enum %s {\n%s\n}") 2709 | else: 2710 | print(json.dumps(all_enums, sort_keys=False)) 2711 | -------------------------------------------------------------------------------- /hearthstone/mercenaryxml.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from typing import Any, Callable, Dict, Iterator, Tuple 3 | 4 | from hearthstone.enums import Rarity 5 | 6 | from .utils import ElementTree 7 | from .xmlutils import download_to_tempfile_retry 8 | 9 | 10 | class MercenaryXML: 11 | 12 | @classmethod 13 | def from_xml(cls, xml): 14 | self = cls(int(xml.attrib["ID"])) 15 | self.collectible = xml.attrib["collectible"].lower() == "true" 16 | self.crafting_cost = int(xml.attrib["crafting_cost"]) 17 | self.name = xml.attrib["name"] 18 | self.rarity = Rarity(int(xml.attrib["rarity"])) 19 | 20 | short_name_elt = xml.find("ShortName") 21 | if len(short_name_elt): 22 | short_name_dict = {} 23 | for loc_element in short_name_elt: 24 | short_name_dict[loc_element.tag] = loc_element.text 25 | 26 | self.short_names = short_name_dict 27 | 28 | skins = xml.find("Skins") 29 | for skin_elt in skins: 30 | skin_dbf_id = int(skin_elt.attrib["CardID"]) 31 | self.skin_dbf_ids.append(skin_dbf_id) 32 | if "default" in skin_elt.attrib and skin_elt.attrib["default"].lower() == "true": 33 | self.default_skin_dbf_id = skin_dbf_id 34 | 35 | specializations = xml.find("Specializations") 36 | for specialization_elt in specializations: 37 | ability_list = [] 38 | abilities_elt = specialization_elt.find("Abilities") 39 | for ability_elt in abilities_elt: 40 | name_elt = ability_elt.find("Name") 41 | ability_name_dict = {} 42 | for loc_element in name_elt: 43 | ability_name_dict[loc_element.tag] = loc_element.text 44 | 45 | tiers_elt = ability_elt.find("Tiers") 46 | tier_list = [] 47 | for tier_elt in tiers_elt: 48 | tier_list.append({ 49 | "crafting_cost": int(tier_elt.attrib["crafting_cost"]), 50 | "dbf_id": int(tier_elt.attrib["CardID"]), 51 | "tier": int(tier_elt.attrib["tier"]) 52 | }) 53 | 54 | ability_list.append({ 55 | "id": int(ability_elt.attrib["ID"]), 56 | "name": ability_name_dict, 57 | "tiers": tier_list 58 | }) 59 | 60 | specialization_name_dict = {} 61 | specialization_names = specialization_elt.find("Name") 62 | for loc_element in specialization_names: 63 | specialization_name_dict[loc_element.tag] = loc_element.text 64 | 65 | self.specializations.append({ 66 | "id": int(specialization_elt.attrib["ID"]), 67 | "name": specialization_name_dict, 68 | "abilities": ability_list 69 | }) 70 | 71 | equipments = xml.find("Equipments") 72 | for equipment_elt in equipments: 73 | tiers_elt = equipment_elt.find("Tiers") 74 | tier_list = [] 75 | for tier_elt in tiers_elt: 76 | tier_list.append({ 77 | "crafting_cost": int(tier_elt.attrib["crafting_cost"]), 78 | "dbf_id": int(tier_elt.attrib["CardID"]), 79 | "tier": int(tier_elt.attrib["tier"]) 80 | }) 81 | 82 | self.equipment.append({ 83 | "id": int(equipment_elt.attrib["ID"]), 84 | "tiers": tier_list, 85 | }) 86 | 87 | return self 88 | 89 | def __init__(self, mercenary_id, locale="enUS"): 90 | self.id = mercenary_id 91 | self.collectible = False 92 | self.crafting_cost = 0 93 | self.name = "" 94 | self.rarity = Rarity.INVALID 95 | 96 | self.default_skin_dbf_id = 0 97 | self.skin_dbf_ids = [] 98 | 99 | self.equipment = [] 100 | self.specializations = [] 101 | 102 | self.short_names = {} 103 | 104 | self.locale = locale 105 | 106 | def to_xml(self): 107 | ret = ElementTree.Element( 108 | "Mercenary", 109 | ID=str(self.id), 110 | collectible=str(self.collectible), 111 | crafting_cost=str(self.crafting_cost), 112 | name=self.name, 113 | rarity=str(int(self.rarity)) 114 | ) 115 | 116 | skins_elt = ElementTree.SubElement(ret, "Skins") 117 | for skin_dbf_id in self.skin_dbf_ids: 118 | skin_elt = ElementTree.SubElement(skins_elt, "Skin", CardID=str(skin_dbf_id)) 119 | if skin_dbf_id == self.default_skin_dbf_id: 120 | skin_elt.attrib["default"] = str(True) 121 | 122 | if len(self.short_names): 123 | short_names_elt = ElementTree.SubElement(ret, "ShortName") 124 | for locale, localized_value in sorted(self.short_names.items()): 125 | if localized_value: 126 | loc_element = ElementTree.SubElement(short_names_elt, locale) 127 | loc_element.text = str(localized_value) 128 | 129 | specializations_elt = ElementTree.SubElement(ret, "Specializations") 130 | for specialization in self.specializations: 131 | spec_elt = ElementTree.SubElement( 132 | specializations_elt, 133 | "Specialization", 134 | ID=str(specialization["id"]) 135 | ) 136 | spec_name = ElementTree.SubElement(spec_elt, "Name") 137 | 138 | for locale, localized_value in sorted(specialization["name"].items()): 139 | if localized_value: 140 | loc_element = ElementTree.SubElement(spec_name, locale) 141 | loc_element.text = str(localized_value) 142 | 143 | abilities_elt = ElementTree.SubElement(spec_elt, "Abilities") 144 | for ability in specialization["abilities"]: 145 | ability_elt = ElementTree.SubElement( 146 | abilities_elt, 147 | "Ability", 148 | ID=str(ability["id"]), 149 | ) 150 | 151 | name_elt = ElementTree.SubElement(ability_elt, "Name") 152 | for locale, localized_value in sorted(ability["name"].items()): 153 | if localized_value: 154 | loc_element = ElementTree.SubElement(name_elt, locale) 155 | loc_element.text = str(localized_value) 156 | 157 | tiers_elt = ElementTree.SubElement(ability_elt, "Tiers") 158 | for tier_dict in sorted(ability["tiers"], key=lambda t: t["tier"]): 159 | ElementTree.SubElement( 160 | tiers_elt, 161 | "Tier", 162 | CardID=str(tier_dict["dbf_id"]), 163 | crafting_cost=str(tier_dict["crafting_cost"]), 164 | tier=str(tier_dict["tier"]) 165 | ) 166 | 167 | equipments_elt = ElementTree.SubElement(ret, "Equipments") 168 | for equipment in self.equipment: 169 | equipment_elt = ElementTree.SubElement( 170 | equipments_elt, 171 | "Equipment", 172 | ID=str(equipment["id"]), 173 | ) 174 | 175 | tiers_elt = ElementTree.SubElement(equipment_elt, "Tiers") 176 | for tier_dict in sorted(equipment["tiers"], key=lambda t: t["tier"]): 177 | ElementTree.SubElement( 178 | tiers_elt, 179 | "Tier", 180 | CardID=str(tier_dict["dbf_id"]), 181 | crafting_cost=str(tier_dict["crafting_cost"]), 182 | tier=str(tier_dict["tier"]) 183 | ) 184 | 185 | return ret 186 | 187 | 188 | mercenary_cache: Dict[Tuple[str, str], Tuple[Dict[int, MercenaryXML], Any]] = {} 189 | 190 | 191 | XML_URL = "https://api.hearthstonejson.com/v1/latest/MercenaryDefs.xml" 192 | 193 | 194 | def _bootstrap_from_web(parse: Callable[[Iterator[Tuple[str, Any]]], None], url=None): 195 | if url is None: 196 | url = XML_URL 197 | 198 | with tempfile.TemporaryFile(mode="rb+") as fp: 199 | if download_to_tempfile_retry(url, fp): 200 | fp.flush() 201 | fp.seek(0) 202 | 203 | parse(ElementTree.iterparse(fp, events=("start", "end",))) 204 | 205 | 206 | def _bootstrap_from_library(parse: Callable[[Iterator[Tuple[str, Any]]], None], path=None): 207 | from hearthstone_data import get_mercenarydefs_path 208 | 209 | if path is None: 210 | path = get_mercenarydefs_path() 211 | 212 | with open(path, "rb") as f: 213 | parse(ElementTree.iterparse(f, events=("start", "end",))) 214 | 215 | 216 | def load(locale="enUS", path=None, url=None): 217 | cache_key = (path, locale) 218 | if cache_key not in mercenary_cache: 219 | db = {} 220 | 221 | def parse(context: Iterator[Tuple[str, Any]]): 222 | nonlocal db 223 | root = None 224 | for action, elem in context: 225 | if action == "start" and elem.tag == "MercenaryDefs": 226 | root = elem 227 | continue 228 | 229 | if action == "end" and elem.tag == "Mercenary": 230 | merc = MercenaryXML.from_xml(elem) 231 | merc.locale = locale 232 | db[merc.id] = merc 233 | 234 | elem.clear() # type: ignore 235 | root.clear() # type: ignore 236 | 237 | if path is None: 238 | _bootstrap_from_web(parse, url=url) 239 | 240 | if not db: 241 | _bootstrap_from_library(parse, path=path) 242 | 243 | mercenary_cache[cache_key] = (db, None) 244 | 245 | return mercenary_cache[cache_key] 246 | -------------------------------------------------------------------------------- /hearthstone/stringsfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hearthstone Strings file 3 | 4 | File format: TSV. Lines starting with `#` are ignored. 5 | Key is always `TAG` 6 | """ 7 | import csv 8 | import json 9 | import sys 10 | import tempfile 11 | from typing import Dict, Optional, Tuple 12 | 13 | from hearthstone.xmlutils import download_to_tempfile_retry 14 | 15 | 16 | StringsRow = Dict[str, str] 17 | StringsDict = Dict[str, StringsRow] 18 | 19 | _cache: Dict[Tuple[str, str], StringsDict] = {} 20 | 21 | 22 | def load_json(fp) -> StringsDict: 23 | hsjson_strings = json.loads(fp.read()) 24 | return {k: {"TEXT": v} for k, v in hsjson_strings.items()} 25 | 26 | 27 | def load_txt(fp) -> StringsDict: 28 | fp = map(lambda x: x.replace("\0", ""), fp) 29 | reader = csv.DictReader( 30 | filter(lambda row: row.strip() and not row.startswith("#"), fp), 31 | delimiter="\t" 32 | ) 33 | stripped_rows = [{k: v for k, v in row.items() if k and v} for row in reader] 34 | return { 35 | stripped_row.pop("TAG"): stripped_row for stripped_row in stripped_rows 36 | if stripped_row 37 | } 38 | 39 | 40 | def _load_globalstrings_from_web(locale="enUS") -> Optional[StringsDict]: 41 | with tempfile.TemporaryFile() as fp: 42 | json_url = "https://api.hearthstonejson.com/v1/strings/%s/GLOBAL.json" % locale 43 | if download_to_tempfile_retry(json_url, fp): 44 | fp.flush() 45 | fp.seek(0) 46 | 47 | return load_json(fp) 48 | else: 49 | return None 50 | 51 | 52 | def _load_globalstrings_from_library(locale="enUS") -> StringsDict: 53 | from hearthstone_data import get_strings_file 54 | 55 | path: str = get_strings_file(locale, filename="GLOBAL.txt") 56 | with open(path, "r", encoding="utf-8-sig") as f: 57 | return load_txt(f) 58 | 59 | 60 | def load_globalstrings(locale="enUS") -> StringsDict: 61 | key = (locale, "GLOBAL.txt") 62 | if key not in _cache: 63 | sd = _load_globalstrings_from_web(locale=locale) 64 | 65 | if not sd: 66 | sd = _load_globalstrings_from_library(locale=locale) 67 | 68 | _cache[key] = sd 69 | 70 | return _cache[key] 71 | 72 | 73 | if __name__ == "__main__": 74 | for path in sys.argv[1:]: 75 | with open(path, "r") as f: 76 | print(json.dumps(load_txt(f))) 77 | -------------------------------------------------------------------------------- /hearthstone/types.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from hearthstone import enums 4 | 5 | 6 | GameTagsDict = Dict[enums.GameTag, int] 7 | -------------------------------------------------------------------------------- /hearthstone/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from ..enums import CardClass, CardSet, Race, Rarity, ZodiacYear 4 | 5 | 6 | try: 7 | from lxml import etree as ElementTree # noqa 8 | except ImportError: 9 | from xml.etree import ElementTree # noqa 10 | 11 | 12 | CARDCLASS_HERO_MAP = { 13 | CardClass.DEATHKNIGHT: "HERO_11", 14 | CardClass.DEMONHUNTER: "HERO_10", 15 | CardClass.DRUID: "HERO_06", 16 | CardClass.HUNTER: "HERO_05", 17 | CardClass.MAGE: "HERO_08", 18 | CardClass.PALADIN: "HERO_04", 19 | CardClass.PRIEST: "HERO_09", 20 | CardClass.ROGUE: "HERO_03", 21 | CardClass.SHAMAN: "HERO_02", 22 | CardClass.WARLOCK: "HERO_07", 23 | CardClass.WARRIOR: "HERO_01", 24 | CardClass.WHIZBANG: "BOT_914h", 25 | } 26 | 27 | 28 | # In the past, card names used to be predictably GLOBAL_CARD_SET_%CARDSET%. However in 29 | # recent expansion, it uses a custom 3 letter set code instead. 30 | CARDSET_GLOBAL_STRING_MAP = { 31 | CardSet.DRAGONS: "GLOBAL_CARD_SET_DRG", 32 | CardSet.YEAR_OF_THE_DRAGON: "GLOBAL_CARD_SET_YOD", 33 | CardSet.DEMON_HUNTER_INITIATE: "GLOBAL_CARD_SET_DHI", 34 | CardSet.BLACK_TEMPLE: "GLOBAL_CARD_SET_BT", 35 | CardSet.SCHOLOMANCE: "GLOBAL_CARD_SET_SCH", 36 | CardSet.DARKMOON_FAIRE: "GLOBAL_CARD_SET_DMF", 37 | CardSet.THE_BARRENS: "GLOBAL_CARD_SET_BAR", 38 | CardSet.STORMWIND: "GLOBAL_CARD_SET_SW", 39 | CardSet.ALTERAC_VALLEY: "GLOBAL_CARD_SET_AV", 40 | CardSet.THE_SUNKEN_CITY: "GLOBAL_CARD_SET_TSC", 41 | CardSet.WILD_WEST: "GLOBAL_CARD_SET_WST", 42 | } 43 | 44 | 45 | # The following dictionary is a consequence of Hearthstone adding multi-race cards. 46 | # 47 | # Before patch 25.0 Hearthstone only supported a single Race tag per card. However, in order 48 | # to support an arbitrary number of Races per card the game developer has introduced a set 49 | # of flag tags, that only exist to signify cards belonging to a specific race. 50 | # 51 | # For example, a card Wisp would be an "Undead Dragon" if it had the tags 52 | # 2534 and 2523 set. However, in practice, one of these is still encoded using the Race tag, 53 | # so likely such a card would have RACE = 11 (UNDEAD) and 2523 = 1 (DRAGON). 54 | # 55 | # If a new race is introduced, you're expected to add the tag here. You can find out the 56 | # mapping by running patch processing and looking at the RaceTagMap.xml in the output 57 | # directory. 58 | CARDRACE_TAG_MAP = { 59 | Race.BLOODELF: 2524, 60 | Race.DRAENEI: 2525, 61 | Race.DWARF: 2526, 62 | Race.GNOME: 2527, 63 | Race.GOBLIN: 2528, 64 | Race.HUMAN: 2529, 65 | Race.NIGHTELF: 2530, 66 | Race.ORC: 2531, 67 | Race.TAUREN: 2532, 68 | Race.TROLL: 2533, 69 | Race.UNDEAD: 2534, 70 | Race.WORGEN: 2535, 71 | Race.GOBLIN2: None, 72 | Race.MURLOC: 2536, 73 | Race.DEMON: 2537, 74 | Race.SCOURGE: 2538, 75 | Race.MECHANICAL: 2539, 76 | Race.ELEMENTAL: 2540, 77 | Race.OGRE: 2541, 78 | Race.BEAST: 2542, 79 | Race.TOTEM: 2543, 80 | Race.NERUBIAN: 2544, 81 | Race.PIRATE: 2522, 82 | Race.DRAGON: 2523, 83 | Race.BLANK: None, 84 | Race.ALL: None, 85 | Race.EGG: 2545, 86 | Race.QUILBOAR: 2546, 87 | Race.CENTAUR: 2547, 88 | Race.FURBOLG: 2548, 89 | Race.HIGHELF: 2549, 90 | Race.TREANT: 2550, 91 | Race.OWLKIN: 2551, 92 | Race.HALFORC: 2552, 93 | Race.LOCK: None, 94 | Race.NAGA: 2553, 95 | Race.OLDGOD: 2554, 96 | Race.PANDAREN: 2555, 97 | Race.GRONN: 2556, 98 | Race.CELESTIAL: 2584, 99 | Race.GNOLL: 2585, 100 | Race.GOLEM: 2586, 101 | Race.HARPY: 2587, 102 | Race.VULPERA: 2588, 103 | # See comment at start of dictionary for how to identify the value for newly added races 104 | } 105 | REVERSE_CARDRACE_TAG_MAP = {v: k for k, v in CARDRACE_TAG_MAP.items()} 106 | 107 | 108 | SECRET_COSTS = { 109 | CardClass.HUNTER: 2, 110 | CardClass.MAGE: 3, 111 | CardClass.PALADIN: 1, 112 | CardClass.ROGUE: 2, 113 | CardClass.WARRIOR: 0, 114 | } 115 | 116 | VISITING_TOURISTS = { 117 | CardClass.DEATHKNIGHT: ["VAC_503"], 118 | CardClass.DRUID: ["VAC_340"], 119 | CardClass.HUNTER: ["VAC_957", "WORK_013"], 120 | CardClass.MAGE: ["VAC_519"], 121 | CardClass.PALADIN: ["VAC_424"], 122 | CardClass.PRIEST: ["VAC_501"], 123 | CardClass.ROGUE: ["VAC_507", "WORK_063"], 124 | CardClass.SHAMAN: ["VAC_437"], 125 | CardClass.WARLOCK: ["VAC_336"], 126 | CardClass.WARRIOR: ["VAC_413"], 127 | CardClass.DEMONHUNTER: ["VAC_450"], 128 | } 129 | 130 | 131 | CRAFTING_COSTS = { 132 | Rarity.COMMON: (40, 400), 133 | Rarity.RARE: (100, 800), 134 | Rarity.EPIC: (400, 1600), 135 | Rarity.LEGENDARY: (1600, 3200), 136 | } 137 | 138 | DISENCHANT_COSTS = { 139 | Rarity.COMMON: (5, 50), 140 | Rarity.RARE: (20, 100), 141 | Rarity.EPIC: (100, 400), 142 | Rarity.LEGENDARY: (400, 1600), 143 | } 144 | 145 | 146 | STANDARD_SETS = { 147 | ZodiacYear.PRE_STANDARD: [ 148 | CardSet.BASIC, CardSet.EXPERT1, CardSet.REWARD, CardSet.PROMO, 149 | CardSet.NAXX, CardSet.GVG, CardSet.BRM, CardSet.TGT, CardSet.LOE, 150 | ], 151 | ZodiacYear.KRAKEN: [ 152 | CardSet.BASIC, CardSet.EXPERT1, 153 | CardSet.BRM, CardSet.TGT, CardSet.LOE, CardSet.OG, CardSet.OG_RESERVE, 154 | CardSet.KARA, CardSet.KARA_RESERVE, CardSet.GANGS, CardSet.GANGS_RESERVE, 155 | ], 156 | ZodiacYear.MAMMOTH: [ 157 | CardSet.BASIC, CardSet.EXPERT1, 158 | CardSet.OG, CardSet.OG_RESERVE, CardSet.KARA, CardSet.KARA_RESERVE, 159 | CardSet.GANGS, CardSet.GANGS_RESERVE, CardSet.UNGORO, CardSet.ICECROWN, 160 | CardSet.LOOTAPALOOZA, 161 | ], 162 | ZodiacYear.RAVEN: [ 163 | CardSet.BASIC, CardSet.EXPERT1, 164 | CardSet.UNGORO, CardSet.ICECROWN, CardSet.LOOTAPALOOZA, CardSet.GILNEAS, 165 | CardSet.BOOMSDAY, CardSet.TROLL, 166 | ], 167 | ZodiacYear.DRAGON: [ 168 | CardSet.BASIC, CardSet.EXPERT1, 169 | CardSet.GILNEAS, CardSet.BOOMSDAY, CardSet.TROLL, CardSet.DALARAN, CardSet.ULDUM, 170 | CardSet.WILD_EVENT, CardSet.DRAGONS, CardSet.YEAR_OF_THE_DRAGON, 171 | CardSet.BLACK_TEMPLE, CardSet.DEMON_HUNTER_INITIATE, 172 | ], 173 | ZodiacYear.PHOENIX: [ 174 | CardSet.BASIC, CardSet.EXPERT1, 175 | CardSet.DALARAN, CardSet.ULDUM, CardSet.WILD_EVENT, CardSet.DRAGONS, 176 | CardSet.YEAR_OF_THE_DRAGON, CardSet.BLACK_TEMPLE, CardSet.DEMON_HUNTER_INITIATE, 177 | CardSet.SCHOLOMANCE, CardSet.DARKMOON_FAIRE, 178 | ], 179 | ZodiacYear.GRYPHON: [ 180 | CardSet.CORE, 181 | CardSet.BLACK_TEMPLE, CardSet.SCHOLOMANCE, CardSet.DARKMOON_FAIRE, 182 | CardSet.THE_BARRENS, CardSet.WAILING_CAVERNS, CardSet.STORMWIND, 183 | CardSet.ALTERAC_VALLEY, 184 | ], 185 | ZodiacYear.HYDRA: [ 186 | CardSet.CORE, 187 | CardSet.THE_BARRENS, CardSet.WAILING_CAVERNS, CardSet.STORMWIND, 188 | CardSet.ALTERAC_VALLEY, CardSet.THE_SUNKEN_CITY, CardSet.REVENDRETH, 189 | CardSet.RETURN_OF_THE_LICH_KING, CardSet.PATH_OF_ARTHAS, 190 | CardSet.BATTLE_OF_THE_BANDS, 191 | ], 192 | ZodiacYear.WOLF: [ 193 | CardSet.CORE, 194 | CardSet.THE_SUNKEN_CITY, CardSet.REVENDRETH, CardSet.RETURN_OF_THE_LICH_KING, 195 | CardSet.PATH_OF_ARTHAS, CardSet.BATTLE_OF_THE_BANDS, CardSet.TITANS, 196 | CardSet.WILD_WEST, CardSet.WHIZBANGS_WORKSHOP, CardSet.EVENT, 197 | ], 198 | ZodiacYear.PEGASUS: [ 199 | CardSet.CORE, 200 | CardSet.BATTLE_OF_THE_BANDS, CardSet.TITANS, CardSet.WILD_WEST, 201 | CardSet.EVENT, CardSet.WHIZBANGS_WORKSHOP, CardSet.ISLAND_VACATION, 202 | CardSet.SPACE, CardSet.EVENT, CardSet.EMERALD_DREAM, CardSet.THE_LOST_CITY 203 | ], 204 | } 205 | 206 | 207 | try: 208 | _EPOCH = datetime.fromtimestamp(0) 209 | except OSError: 210 | # https://bugs.python.org/issue29097 (Windows-only) 211 | _EPOCH = datetime.fromtimestamp(86400) 212 | 213 | 214 | ZODIAC_ROTATION_DATES = { 215 | ZodiacYear.PRE_STANDARD: _EPOCH, 216 | ZodiacYear.KRAKEN: datetime(2016, 4, 26), 217 | ZodiacYear.MAMMOTH: datetime(2017, 4, 7), 218 | ZodiacYear.RAVEN: datetime(2018, 4, 12), 219 | ZodiacYear.DRAGON: datetime(2019, 4, 9), 220 | ZodiacYear.PHOENIX: datetime(2020, 4, 7), 221 | ZodiacYear.GRYPHON: datetime(2021, 3, 30), 222 | ZodiacYear.HYDRA: datetime(2022, 4, 12), 223 | ZodiacYear.WOLF: datetime(2023, 4, 11), 224 | ZodiacYear.PEGASUS: datetime(2024, 3, 19), 225 | } 226 | 227 | 228 | # QuestController.cs 229 | QUEST_REWARDS = { 230 | "UNG_940": "UNG_940t8", 231 | "UNG_954": "UNG_954t1", 232 | "UNG_934": "UNG_934t1", 233 | "UNG_829": "UNG_829t1", 234 | "UNG_028": "UNG_028t", 235 | "UNG_067": "UNG_067t1", 236 | "UNG_116": "UNG_116t", 237 | "UNG_920": "UNG_920t1", 238 | "UNG_942": "UNG_942t", 239 | } 240 | 241 | 242 | # GameplayStringTextBuilder.cs 243 | 244 | SPELLSTONE_STRINGS = { 245 | "LOOT_043": "GAMEPLAY_AMETHYST_SPELLSTONE_%d", 246 | "LOOT_051": "GAMEPLAY_JASPER_SPELLSTONE_%d", 247 | "LOOT_064": "GAMEPLAY_SAPPHIRE_SPELLSTONE_%d", 248 | "LOOT_091": "GAMEPLAY_PEARL_SPELLSTONE_%d", 249 | "LOOT_103": "GAMEPLAY_RUBY_SPELLSTONE_%d", 250 | "LOOT_503": "GAMEPLAY_ONYX_SPELLSTONE_%d", 251 | "LOOT_507": "GAMEPLAY_DIAMOND_SPELLSTONE_%d", 252 | "LOOT_526d": "GAMEPLAY_LOOT_526d_DARKNESS_%d", 253 | } 254 | 255 | 256 | UPGRADABLE_CARDS_MAP = { 257 | # Fatespinner 258 | "ICC_047t": "ICC_047", 259 | "ICC_047t2": "ICC_047", 260 | # Lesser Amethyst Spellstone 261 | "LOOT_043t2": "LOOT_043", 262 | "LOOT_043t3": "LOOT_043", 263 | # Lesser Jasper Spellstone 264 | "LOOT_051t1": "LOOT_051", 265 | "LOOT_051t2": "LOOT_051", 266 | # Lesser Sapphire Spellstone 267 | "LOOT_064t1": "LOOT_064", 268 | "LOOT_064t2": "LOOT_064", 269 | # Lesser Emerald Spellstone 270 | "LOOT_080t2": "LOOT_080", 271 | "LOOT_080t3": "LOOT_080", 272 | # Lesser Pearl Spellstone 273 | "LOOT_091t1": "LOOT_091", 274 | "LOOT_091t2": "LOOT_091", 275 | # Lesser Ruby Spellstone 276 | "LOOT_103t1": "LOOT_103", 277 | "LOOT_103t2": "LOOT_103", 278 | # Lesser Mithril Spellstone 279 | "LOOT_203t2": "LOOT_203", 280 | "LOOT_203t3": "LOOT_203", 281 | # Unidentified Elixier 282 | "LOOT_278t1": "LOOT_278", 283 | "LOOT_278t2": "LOOT_278", 284 | "LOOT_278t3": "LOOT_278", 285 | "LOOT_278t4": "LOOT_278", 286 | # Unidentified Shield 287 | "LOOT_285t": "LOOT_285", 288 | "LOOT_285t2": "LOOT_285", 289 | "LOOT_285t3": "LOOT_285", 290 | "LOOT_285t4": "LOOT_285", 291 | # Unidentified Maul 292 | "LOOT_286t1": "LOOT_286", 293 | "LOOT_286t2": "LOOT_286", 294 | "LOOT_286t3": "LOOT_286", 295 | "LOOT_286t4": "LOOT_286", 296 | # Lesser Onyx Spellstone 297 | "LOOT_503t": "LOOT_503", 298 | "LOOT_503t2": "LOOT_503", 299 | # Lesser Diamond Spellstone 300 | "LOOT_507t": "LOOT_507", 301 | "LOOT_507t2": "LOOT_507", 302 | # Duskhaven Hunter 303 | "GIL_200t": "GIL_200", 304 | # Pumpkin Peasant 305 | "GIL_201t": "GIL_201", 306 | # Gilnean Royal Guard 307 | "GIL_202t": "GIL_202", 308 | # Swift Messenger 309 | "GIL_528t": "GIL_528", 310 | # Spellshifter 311 | "GIL_529t": "GIL_529", 312 | # Unidentified Contract 313 | "DAL_366t1": "DAL_366", 314 | "DAL_366t2": "DAL_366", 315 | "DAL_366t3": "DAL_366", 316 | "DAL_366t4": "DAL_366", 317 | # Galakrond 318 | "DRG_600t2": "DRG_600", 319 | "DRG_600t3": "DRG_600", 320 | "DRG_610t2": "DRG_610", 321 | "DRG_610t3": "DRG_610", 322 | "DRG_620t2": "DRG_620", 323 | "DRG_620t3": "DRG_620", 324 | "DRG_650t2": "DRG_650", 325 | "DRG_650t3": "DRG_650", 326 | "DRG_660t2": "DRG_660", 327 | "DRG_660t3": "DRG_660", 328 | # Corrupted Card 329 | "DMF_061t": "DMF_061", # Faire Arborist 330 | "DMF_730t": "DMF_730", # Moontouched Amulet 331 | "DMF_083t": "DMF_083", # Dancing Cobra 332 | "DMF_101t": "DMF_101", # Firework Elemental 333 | "DMF_054t": "DMF_054", # Insight 334 | "DMF_184t": "DMF_184", # Fairground Fool 335 | "DMF_517a": "DMF_517", # Sweet Tooth 336 | "DMF_703t": "DMF_703", # Pit Master 337 | "DMF_526a": "DMF_526", # Stage Dive 338 | "DMF_073t": "DMF_073", # Darkmoon Dirigible 339 | "DMF_082t": "DMF_082", # Darkmoon Statue 340 | "DMF_174t": "DMF_174", # Circus Medic 341 | "DMF_163t": "DMF_163", # Carnival Clown 342 | # Cascading Disaster 343 | "DMF_117t2": "DMF_117", 344 | "DMF_117t": "DMF_117", 345 | "DMF_078t": "DMF_078", # Strongman 346 | "DMF_186a": "DMF_186", # Auspicious Spirits 347 | "DMF_118t": "DMF_118", # Tickatus 348 | "DMF_247t": "DMF_247", # Insatiable Felhound 349 | "DMF_248t": "DMF_248", # Felsteel Executioner 350 | "DMF_064t": "DMF_064", # Carousel Gryphon 351 | "DMF_124t": "DMF_124", # Horrendous Growth 352 | "DMF_090t": "DMF_090", # Don't Feed the Animals 353 | "DMF_105t": "DMF_105", # Ring Toss 354 | "DMF_701t": "DMF_701", # Dunk Tank 355 | "DMF_080t": "DMF_080", # Fleethoof Pearltusk 356 | "DMF_244t": "DMF_244", # Day at the Faire 357 | # Tame Beast 358 | "BAR_034t": "BAR_034", 359 | "BAR_034t2": "BAR_034", 360 | # Chain Lightning 361 | "BAR_044t": "BAR_044", 362 | "BAR_044t2": "BAR_044", 363 | # Flurry 364 | "BAR_305t": "BAR_305", 365 | "BAR_305t2": "BAR_305", 366 | # Condemn 367 | "BAR_314t": "BAR_314", 368 | "BAR_314t2": "BAR_314", 369 | # Wicked Stab 370 | "BAR_319t": "BAR_319", 371 | "BAR_319t2": "BAR_319", 372 | # Living Seed 373 | "BAR_536t": "BAR_536", 374 | "BAR_536t2": "BAR_536", 375 | # Conviction 376 | "BAR_880t": "BAR_880", 377 | "BAR_880t2": "BAR_880", 378 | # Conditioning 379 | "BAR_842t": "BAR_842", 380 | "BAR_842t2": "BAR_842", 381 | # Fury 382 | "BAR_891t": "BAR_891", 383 | "BAR_891t2": "BAR_891", 384 | # Imp Swarm 385 | "BAR_914t": "BAR_914", 386 | "BAR_914t2": "BAR_914", 387 | # Harmonic / Dissonant cards 388 | "ETC_314t": "ETC_314", # Harmonic Pop 389 | "ETC_379t": "ETC_379", # Harmonic Mood 390 | "ETC_427t": "ETC_427", # Harmonic Metal 391 | "ETC_506t": "ETC_506", # Harmonic Disco 392 | "ETC_717t": "ETC_717", # Harmonic Hip Hop 393 | # Remixed Dispense-o-bot 394 | "JAM_000t": "JAM_000", # Chilling Dispense-o-bot 395 | "JAM_000t2": "JAM_000", # Merch Dispense-o-bot 396 | "JAM_000t3": "JAM_000", # Money Dispense-o-bot 397 | "JAM_000t4": "JAM_000", # Mystery Dispense-o-bot 398 | # Remixed Totemcarver 399 | "JAM_012t": "JAM_012", # Loud Totemcarver 400 | "JAM_012t2": "JAM_012", # Bluesy Totemcarver 401 | "JAM_012t3": "JAM_012", # Blazing Totemcarver 402 | "JAM_012t4": "JAM_012", # Karaoke Totemcarver 403 | # Remixed Tuning Fork 404 | "JAM_015t": "JAM_015", # Sharpened Tuning Fork 405 | "JAM_015t2": "JAM_015", # Reinforced Tuning Fork 406 | "JAM_015t3": "JAM_015", # Curved Tuning Fork 407 | "JAM_015t4": "JAM_015", # Backup Tuning Fork 408 | # Remixed Rhapsody 409 | "JAM_018t": "JAM_018", # Angsty Rhapsody 410 | "JAM_018t2": "JAM_018", # Resounding Rhapsody 411 | "JAM_018t3": "JAM_018", # Emotional Rhapsody 412 | "JAM_018t4": "JAM_018", # Wailing Rhapsody 413 | # Remixed Musician 414 | "JAM_033t": "JAM_033", # Cathedral Musician 415 | "JAM_033t2": "JAM_033", # Tropical Musician 416 | "JAM_033t3": "JAM_033", # Romantic Musician 417 | "JAM_033t4": "JAM_033", # Noise Musician 418 | # Lesser Opal Spellstone 419 | "TOY_645t": "TOY_645", 420 | "TOY_645t1": "TOY_645", 421 | # Lesser Spinel Spellstone 422 | "TOY_825t": "TOY_825", 423 | "TOY_825t2": "TOY_825", 424 | # Blossoms 425 | "TTN_950t3": "TTN_950", # Forest Seedlings 426 | "TTN_930t": "TTN_930", # Frost Lotus Seedling 427 | # Zilliax Deluxe 3000 428 | "TOY_330t5": "TOY_330", 429 | "TOY_330t6": "TOY_330", 430 | "TOY_330t7": "TOY_330", 431 | "TOY_330t8": "TOY_330", 432 | "TOY_330t9": "TOY_330", 433 | "TOY_330t10": "TOY_330", 434 | "TOY_330t11": "TOY_330", 435 | "TOY_330t12": "TOY_330", 436 | } 437 | 438 | 439 | def get_original_card_id(card_id): 440 | # Transfer Student 441 | if str(card_id).startswith("SCH_199t"): 442 | return "SCH_199" 443 | return UPGRADABLE_CARDS_MAP.get(card_id, card_id) 444 | 445 | 446 | # A map of card ids that have been reprinted in the Wild format. 447 | # Generated via scripts/dump_reprints.py 448 | COPIED_CARDS_MAP_WILD = { 449 | "CORE_AT_021": "AT_021", 450 | "CORE_AT_061": "AT_061", 451 | "CORE_EX1_007": "WON_357", 452 | "CORE_KAR_065": "KAR_065", 453 | "CORE_LOE_012": "LOE_012", 454 | "EX1_007": "WON_357", 455 | "EX1_320": "WON_323", 456 | "EX1_354": "WON_048", 457 | "EX1_609": "WON_018", 458 | "NEW1_012": "WON_031", 459 | "VAN_NEW1_012": "WON_031", 460 | "WON_003": "AT_041", 461 | "WON_009": "OG_313", 462 | "WON_010": "OG_188", 463 | "WON_011": "GVG_035", 464 | "WON_012": "AT_045", 465 | "WON_021": "AT_062", 466 | "WON_022": "LOE_105", 467 | "WON_023": "AT_061", 468 | "WON_024": "AT_063", 469 | "WON_025": "AT_063t", 470 | "WON_029": "AT_006", 471 | "WON_033": "GVG_123", 472 | "WON_035": "GVG_004", 473 | "WON_036": "OG_087", 474 | "WON_037": "OG_090", 475 | "WON_038": "GVG_007", 476 | "WON_045": "KAR_057", 477 | "WON_046": "CFM_639", 478 | "WON_049": "AT_078", 479 | "WON_056": "LOE_006", 480 | "WON_057": "KAR_204", 481 | "WON_058": "AT_012", 482 | "WON_061": "AT_014", 483 | "WON_062": "GVG_009", 484 | "WON_063": "AT_018", 485 | "WON_067": "CFM_691", 486 | "WON_070": "CFM_690", 487 | "WON_071": "AT_033", 488 | "WON_073": "BRM_008", 489 | "WON_075": "OG_282", 490 | "WON_076": "AT_036", 491 | "WON_081": "AT_046", 492 | "WON_082": "CFM_707", 493 | "WON_083": "KAR_021", 494 | "WON_084": "CFM_312", 495 | "WON_085": "AT_049", 496 | "WON_086": "CFM_310", 497 | "WON_093": "AT_024", 498 | "WON_095": "GVG_015", 499 | "WON_096": "LOE_023", 500 | "WON_097": "OG_116", 501 | "WON_098": "KAR_205", 502 | "WON_099": "AT_021", 503 | "WON_100": "AT_025", 504 | "WON_105": "OG_121", 505 | "WON_108": "CFM_754", 506 | "WON_110": "CFM_752", 507 | "WON_111": "OG_301", 508 | "WON_114": "GVG_056", 509 | "WON_117": "CFM_643", 510 | "WON_118": "CFM_715", 511 | "WON_124": "OG_284", 512 | "WON_125": "OG_283", 513 | "WON_127": "OG_162", 514 | "WON_128": "FP1_012", 515 | "WON_130": "CFM_649", 516 | "WON_131": "OG_321", 517 | "WON_133": "BRM_028", 518 | "WON_134": "OG_131", 519 | "WON_135": "OG_280", 520 | "WON_136": "CFM_902", 521 | "WON_137": "CFM_685", 522 | "WON_162": "GVG_046", 523 | "WON_300": "CFM_816", 524 | "WON_302": "OG_202", 525 | "WON_303": "CFM_343", 526 | "WON_304": "OG_293", 527 | "WON_305": "KAR_065", 528 | "WON_306": "GVG_073", 529 | "WON_307": "CFM_336", 530 | "WON_308": "CFM_760", 531 | "WON_309": "KAR_077", 532 | "WON_310": "OG_310", 533 | "WON_311": "LOE_017", 534 | "WON_312": "CFM_815", 535 | "WON_313": "OG_334", 536 | "WON_314": "GVG_011", 537 | "WON_315": "OG_234", 538 | "WON_316": "AT_028", 539 | "WON_317": "OG_330", 540 | "WON_318": "AT_034", 541 | "WON_320": "AT_048", 542 | "WON_321": "AT_050", 543 | "WON_322": "OG_302", 544 | "WON_324": "CFM_750", 545 | "WON_325": "GVG_050", 546 | "WON_326": "CFM_631", 547 | "WON_328": "AT_090", 548 | "WON_329": "BRM_034", 549 | "WON_330": "OG_295", 550 | "WON_331": "CFM_321", 551 | "WON_332": "CFM_852", 552 | "WON_333": "OG_311", 553 | "WON_334": "AT_079", 554 | "WON_335": "FP1_025", 555 | "WON_336": "OG_209", 556 | "WON_337": "KAR_091", 557 | "WON_338": "BRM_016", 558 | "WON_339": "CFM_756", 559 | "WON_340": "LOE_012", 560 | "WON_341": "AT_001", 561 | "WON_342": "AT_015", 562 | "WON_344": "AT_007", 563 | "WON_347": "CFM_334", 564 | "WON_350": "CFM_940", 565 | "WON_351": "CFM_325", 566 | "WON_365": "CFM_039", 567 | "WON_366": "CFM_665" 568 | } 569 | 570 | 571 | def get_copied_card_id_by_format(card_id, format_type): 572 | """Returns a suitable version for stat deduplication in the given FormatType.""" 573 | if format_type == 1: 574 | return COPIED_CARDS_MAP_WILD.get(card_id, card_id) 575 | return card_id 576 | 577 | 578 | SCHEME_CARDS = [ 579 | "DAL_007", # Rafaam's Scheme 580 | "DAL_008", # Dr. Boom's Scheme 581 | "DAL_009", # Hagatha's Scheme 582 | "DAL_010", # Tagwaggle's Scheme 583 | "DAL_011", # Lazul's Scheme 584 | ] 585 | 586 | MAESTRA_DISGUISE_DBF_ID = 64674 587 | 588 | 589 | if __name__ == "__main__": 590 | def _print_cs_dicts(dicts_and_names, tl_format, format): 591 | ret = [] 592 | linefmt = "\t\t{ %d, %s }" 593 | for name, dict in dicts_and_names: 594 | keytype = int 595 | valtype = list(dict.values())[0].__class__ 596 | 597 | lines = ",\n".join( 598 | linefmt % (keytype(key), valtype(value)) 599 | for key, value in dict.items() 600 | if key is not None 601 | ) 602 | ret.append(format % (name, lines)) 603 | 604 | lines = "\n\n".join(ret) 605 | print(tl_format % (lines)) 606 | 607 | print("using System.Collections.Generic;\n") 608 | 609 | _print_cs_dicts( 610 | [ 611 | ("TagRaceMap", REVERSE_CARDRACE_TAG_MAP) 612 | ], 613 | "public static class RaceUtils {\n%s\n}", 614 | "\tpublic static Dictionary %s = new Dictionary() {\n%s\n\t};", 615 | ) 616 | -------------------------------------------------------------------------------- /hearthstone/xmlutils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class RetryException(Exception): 5 | pass 6 | 7 | 8 | def download_to_tempfile(url: str, fp) -> bool: 9 | try: 10 | with requests.get(url, stream=True) as r: 11 | if r.ok: 12 | for chunk in r.iter_content(chunk_size=8192): 13 | fp.write(chunk) 14 | 15 | return True 16 | elif 500 <= r.status_code < 600: 17 | raise RetryException() 18 | else: 19 | return False 20 | except requests.exceptions.RequestException: 21 | raise RetryException() 22 | 23 | 24 | def download_to_tempfile_retry(url: str, fp, retries: int = 3) -> bool: 25 | assert retries >= 0 26 | 27 | try: 28 | return download_to_tempfile(url, fp) 29 | except RetryException: 30 | if retries: 31 | return download_to_tempfile_retry(url, fp, retries - 1) 32 | 33 | return False 34 | -------------------------------------------------------------------------------- /scripts/dump_reprints.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict, defaultdict 2 | 3 | from hearthstone.cardxml import load 4 | from hearthstone.enums import CardSet, GameTag 5 | 6 | 7 | def dump_reprints(): 8 | db, _ = load() 9 | dbf_db = {v.dbf_id: v for k, v in db.items()} 10 | pointers = {} # dbfId -> dict 11 | 12 | # First, assemble a list of mappings from card -> copies 13 | for card in db.values(): 14 | copy_of_dbf_id = card.tags.get(GameTag.DECK_RULE_COUNT_AS_COPY_OF_CARD_ID) 15 | 16 | if not copy_of_dbf_id: 17 | continue 18 | 19 | if copy_of_dbf_id not in dbf_db: 20 | continue 21 | 22 | copy_card = dbf_db[copy_of_dbf_id] 23 | if not card.is_functional_duplicate_of(copy_card): 24 | continue 25 | 26 | pointers[card.dbf_id] = copy_of_dbf_id 27 | 28 | # At this point we have a mapping of dbfId -> dbfId 29 | # Now, try to merge these into sets 30 | 31 | chains = defaultdict(set) 32 | for k, v in pointers.items(): 33 | chains[k].add(k) 34 | chains[k].add(v) 35 | 36 | # Now, keep merging 37 | while True: 38 | # Start over 39 | 40 | modified = False 41 | for parent, targets in chains.items(): 42 | new_targets = set(targets) 43 | 44 | # check if any children own lists 45 | for child in targets: 46 | if child == parent: 47 | continue 48 | if child in chains: 49 | new_targets.update(chains[child]) 50 | del chains[child] 51 | modified = True 52 | for k, chain in list(chains.items()): 53 | if child in chain and k != parent: 54 | new_targets.update(chain) 55 | del chains[k] 56 | modified = True 57 | 58 | # Find the smallest 59 | smallest = min(targets) 60 | 61 | # If the parent is the smallest, nothing to do - children will turn up 62 | if smallest == parent: 63 | targets.update(new_targets) 64 | if modified: 65 | break 66 | else: 67 | continue 68 | 69 | chains[smallest] = new_targets 70 | del chains[parent] 71 | 72 | modified = True 73 | break 74 | 75 | if not modified: 76 | break 77 | 78 | the_map = {} 79 | 80 | for chain in chains.values(): 81 | # Map to cards 82 | the_chain = [dbf_db[c] for c in chain] 83 | the_chain = [c for c in the_chain if c.collectible] 84 | if len(the_chain) < 2: 85 | continue 86 | 87 | # Get rid of chains without WONDERS cards 88 | if not any([c for c in the_chain if c.card_set == CardSet.WONDERS]): 89 | continue 90 | 91 | # Find the best owner 92 | bad_sets = [ 93 | CardSet.CORE, 94 | CardSet.PLACEHOLDER_202204, 95 | CardSet.EXPERT1, 96 | CardSet.BASIC, 97 | CardSet.LEGACY, 98 | CardSet.VANILLA 99 | ] 100 | owners_from_good_sets = [c for c in the_chain if c.card_set not in bad_sets] 101 | 102 | winner = None 103 | 104 | assert len(owners_from_good_sets) in (1, 2) 105 | 106 | if len(owners_from_good_sets) == 1: 107 | # WONDERS is the only good set, map all others to it 108 | assert owners_from_good_sets[0].card_set == CardSet.WONDERS 109 | winner = owners_from_good_sets[0] 110 | elif len(owners_from_good_sets) == 2: 111 | # Probably one is from WON 112 | old_cards = [c for c in owners_from_good_sets if c.card_set != CardSet.WONDERS] 113 | assert len(old_cards) == 1 114 | winner = old_cards[0] 115 | 116 | for c in the_chain: 117 | if c.id == winner.id: 118 | continue 119 | the_map[c.id] = winner.id 120 | 121 | print(dict(OrderedDict(sorted(the_map.items())))) 122 | 123 | 124 | if __name__ == "__main__": 125 | dump_reprints() 126 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = hearthstone 3 | version = 9.16.0 4 | description = CardDefs.xml parser and Hearthstone enums for Python 5 | long_description = file: README.md 6 | long_description_content_type=text/markdown 7 | author = Jerome Leclanche 8 | author_email = jerome@leclan.ch 9 | url = https://github.com/HearthSim/python-hearthstone/ 10 | download_url = https://github.com/HearthSim/python-hearthstone/tarball/master 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: MIT License 15 | Programming Language :: Python 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.6 18 | Topic :: Games/Entertainment 19 | 20 | [options] 21 | packages = find: 22 | include_package_data = True 23 | zip_safe = True 24 | install_requires = 25 | requests 26 | python_requires = >=3.6 27 | 28 | [options.packages.find] 29 | exclude = 30 | tests 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/python-hearthstone/cc13a17a336b5c0edae3557991bbadc835dbabfa/tests/__init__.py -------------------------------------------------------------------------------- /tests/res/DECK_RULESET_RULE_SUBSET.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ziY6RY+E/zCQ496Av7HKSCR+zls= 4 | 5 | 6 | 7 | 5 8 | 6 9 | 10 | 11 | 15 12 | 6 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test_bountyxml.py: -------------------------------------------------------------------------------- 1 | from hearthstone import bountyxml 2 | 3 | 4 | def test_bountyxml_load(): 5 | bounty_db, _ = bountyxml.load() 6 | 7 | assert bounty_db 8 | 9 | assert bounty_db[68].boss_name == "Elris Gloomstalker" 10 | assert bounty_db[58].region_name == "The Barrens" 11 | -------------------------------------------------------------------------------- /tests/test_cardxml.py: -------------------------------------------------------------------------------- 1 | from hearthstone import cardxml 2 | from hearthstone.enums import GameTag, Race 3 | 4 | 5 | def test_cardxml_load(): 6 | cardid_db, _ = cardxml.load() 7 | dbf_db, _ = cardxml.load_dbf() 8 | 9 | assert cardid_db 10 | assert dbf_db 11 | 12 | for card_id, card in cardid_db.items(): 13 | assert dbf_db[card.dbf_id].id == card_id 14 | 15 | for dbf_id, card in dbf_db.items(): 16 | assert cardid_db[card.id].dbf_id == dbf_id 17 | 18 | assert cardid_db["EX1_001"].quest_reward == "" 19 | assert cardid_db["UNG_940"].quest_reward == "UNG_940t8" 20 | 21 | 22 | def test_races(): 23 | card = cardxml.CardXML("EX1_001") 24 | card.tags[GameTag.CARDRACE] = Race.UNDEAD 25 | card.tags[Race.DRAGON.race_tag] = 1 26 | assert card.races == [ 27 | Race.UNDEAD, 28 | Race.DRAGON, 29 | ] 30 | -------------------------------------------------------------------------------- /tests/test_dbf.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from collections import OrderedDict 3 | from io import BytesIO 4 | 5 | from hearthstone.dbf import Dbf 6 | 7 | 8 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def get_resource(path): 12 | return os.path.join(BASE_DIR, "res", path) 13 | 14 | 15 | def test_dbf(): 16 | path = get_resource("DECK_RULESET_RULE_SUBSET.xml") 17 | 18 | dbf = Dbf.load(path) 19 | assert dbf.name == "DECK_RULESET_RULE_SUBSET" 20 | assert dbf.source_fingerprint == "ziY6RY+E/zCQ496Av7HKSCR+zls=" 21 | assert dbf.columns == OrderedDict([ 22 | ("DECK_RULESET_RULE_ID", "Int"), 23 | ("SUBSET_ID", "Int"), 24 | ]) 25 | assert dbf.records == [ 26 | {"DECK_RULESET_RULE_ID": 5, "SUBSET_ID": 6}, 27 | {"DECK_RULESET_RULE_ID": 15, "SUBSET_ID": 6}, 28 | ] 29 | 30 | dbf2 = Dbf() 31 | dbf2.populate(BytesIO(dbf.to_xml())) 32 | assert dbf2.source_fingerprint == dbf.source_fingerprint 33 | assert dbf2.columns == dbf.columns 34 | 35 | for r1, r2 in zip(dbf.records, dbf2.records): 36 | assert r1 == r2 37 | -------------------------------------------------------------------------------- /tests/test_deckstrings.py: -------------------------------------------------------------------------------- 1 | from hearthstone import deckstrings 2 | from hearthstone.enums import FormatType 3 | 4 | 5 | TEST_DECKSTRING_PRE_SIDEBOARD = ( 6 | "AAEBAR8G+LEChwTmwgKhwgLZwgK7BQzquwKJwwKOwwKTwwK5tAK1A/4MqALsuwLrB86uAu0JAA==" 7 | ) 8 | TEST_DECKSTRING = ( 9 | "AAEBAR8GhwS7BfixAqHCAtnCAubCAgyoArUD6wftCf4Mzq4CubQC6rsC7LsCicMCjsMCk8MCAAA=" 10 | ) 11 | TEST_DECKSTRING_CARDLIST = [ 12 | (40426, 2), # Alleycat 13 | (41353, 2), # Jeweled Macaw 14 | (39160, 1), # Cat Trick 15 | (41358, 2), # Crackling Razormaw 16 | (41363, 2), # Dinomancy 17 | (519, 1), # Freezing Trap 18 | (39481, 2), # Kindly Grandmother 19 | (41318, 1), # Stubborn Gastropod 20 | (437, 2), # Animal Companion 21 | (1662, 2), # Eaglehorn Bow 22 | (41249, 1), # Eggnapper 23 | (296, 2), # Kill Command 24 | (40428, 2), # Rat Pack 25 | (1003, 2), # Houndmaster 26 | (38734, 2), # Infested Wolf 27 | (41305, 1), # Nesting Roc 28 | (699, 1), # Tundra Rhino 29 | (1261, 2), # Savannah Highmane 30 | ] 31 | 32 | TEST_SIDEBOARD_DECKSTRING = ( 33 | "AAEBAZCaBgjlsASotgSX7wTvkQXipAX9xAXPxgXGxwUQvp8EobYElrcE+dsEuNwEutwE9v" 34 | "AEhoMFopkF4KQFlMQFu8QFu8cFuJ4Gz54G0Z4GAAED8J8E/cQFuNkE/cQF/+EE/cQFAAA=" 35 | ) 36 | TEST_SIDEBOARD_DECKSTRING_CARDLIST = [ 37 | (102223, 2), # Armor Vendor 38 | (69566, 2), # Psychic Conjurer 39 | (102200, 2), # Shard of the Naaru 40 | (71781, 1), # Sir Finley, Sea Guide 41 | (77305, 2), # The Light! It Burns! 42 | (86626, 1), # Astalor Bloodsworn 43 | (91078, 1), # Audio Amplifier 44 | (102225, 2), # Dirty Rat 45 | (90644, 2), # Mind Eater 46 | (91067, 2), # Power Chord: Synchronize 47 | (82310, 2), # Cathedral of Atonement 48 | (77368, 2), # Identity Theft 49 | (90959, 1), # Love Everlasting 50 | (85154, 2), # Nerubian Vizier 51 | (79767, 1), # Prince Renathal 52 | (86624, 2), # Cannibalize 53 | (79990, 2), # Demolition Renovator 54 | (90749, 1), # E.T.C., Band Manager 55 | (72598, 2), # School Teacher 56 | (77370, 2), # Clean the Scene 57 | (90683, 2), # Harmonic Pop 58 | (84207, 1), # Sister Svalna 59 | (72488, 1), # Blackwater Behemoth 60 | (72481, 2), # Whirlpool 61 | ] 62 | TEST_SIDEBOARD_DECKSTRING_SIDEBOARD = [ 63 | (76984, 1, 90749), 64 | (78079, 1, 90749), 65 | (69616, 1, 90749), 66 | ] 67 | 68 | 69 | DECKSTRING_TEST_DATA = [ 70 | { 71 | "cards": [(1, 2), (2, 2), (3, 2), (4, 2)], 72 | "heroes": [7], # Garrosh Hellscream 73 | "format": FormatType.FT_STANDARD, 74 | "deckstring": "AAECAQcABAECAwQAAA==", 75 | }, 76 | { 77 | "cards": [(8, 1), (179, 1), (2009, 1)], 78 | "heroes": [7], 79 | "format": FormatType.FT_STANDARD, 80 | "deckstring": "AAECAQcDCLMB2Q8AAAA=", 81 | }, 82 | { 83 | "cards": [(1, 3), (2, 3), (3, 3), (4, 3)], 84 | "heroes": [7], # Garrosh Hellscream 85 | "format": FormatType.FT_WILD, 86 | "deckstring": "AAEBAQcAAAQBAwIDAwMEAwA=", 87 | }, 88 | { 89 | "cards": [(1, 1), (2, 1), (3, 1), (4, 1)], 90 | "heroes": [40195], # Maiev Shadowsong 91 | "format": FormatType.FT_WILD, 92 | "deckstring": "AAEBAYO6AgQBAgMEAAAA", 93 | }, 94 | { 95 | # https://hsreplay.net/decks/mae2HTeLYbTIrSYZiALN9d/ 96 | "cards": [ 97 | (41323, 2), # Fire Fly 98 | (376, 2), # Inner Fire 99 | (1650, 2), # Northshire Cleric 100 | (40373, 1), # Potion of Madness 101 | (613, 2), # Power Word: Shield 102 | (1361, 2), # Divine Spirit 103 | (41176, 2), # Radiant Elemental 104 | (41169, 2), # Shadow Visions 105 | (1367, 2), # Shadow Word: Pain 106 | (40432, 2), # Kabal Talonpriest 107 | (1363, 2), # Shadow Word: Death 108 | (41418, 2), # Tar Creeper 109 | (41241, 2), # Tol'vir Stoneshaper 110 | (41180, 1), # Tortollan Shellraiser 111 | (42046, 1), # Lyra the Sunshard 112 | (41410, 2), # Servant of Kalimos 113 | (41928, 1), # Blazecaller 114 | ], 115 | "format": FormatType.FT_STANDARD, 116 | "heroes": [41887], # Tyrande Whisperwind 117 | "deckstring": ( 118 | "AAECAZ/HAgS1uwLcwQLIxwK+yAIN+ALlBNEK0wrXCvIM8LsC0cEC2MECmcIC68ICwsMCysMCAAA=" 119 | ) 120 | }, 121 | { 122 | "cards": [ 123 | (455, 1), 124 | (585, 1), 125 | (699, 1), 126 | (921, 1), 127 | (985, 1), 128 | (1144, 1), 129 | (141, 2), 130 | (216, 2), 131 | (296, 2), 132 | (437, 2), 133 | (519, 2), 134 | (658, 2), 135 | (877, 2), 136 | (1003, 2), 137 | (1243, 2), 138 | (1261, 2), 139 | (1281, 2), 140 | (1662, 2) 141 | ], 142 | "format": FormatType.FT_STANDARD, 143 | "heroes": [31], # Rexxar 144 | "deckstring": ( 145 | "AAECAR8GxwPJBLsFmQfZB/gIDI0B2AGoArUDhwSSBe0G6wfbCe0JgQr+DAAA" 146 | ), 147 | }, 148 | { 149 | "cards": [ 150 | (80647, 2), 151 | (80818, 1), 152 | (91251, 2), 153 | (95344, 2), 154 | (98285, 1), 155 | (100619, 1), 156 | (101015, 2), 157 | (101016, 1), 158 | (101265, 2), 159 | (101375, 1), 160 | (102418, 2), 161 | (102983, 1), 162 | (104634, 2), 163 | (104636, 2), 164 | (104694, 2), 165 | (105355, 2), 166 | (111315, 1), 167 | (111318, 1), 168 | (111319, 2), 169 | ], 170 | "format": FormatType.FT_STANDARD, 171 | "heroes": [78065], 172 | "sideboards": [ 173 | (110440, 1, 102983), # incorrectly sorted 174 | (104947, 1, 102983), 175 | (104950, 1, 102983), 176 | ], 177 | "deckstring": ( 178 | "AAECAfHhBAiy9wTt/wWLkgaYlQb/lwbHpAbT5QbW5QYLh/YE88gF8OgFl5UGkZcGkqAGurEGvLEG9r" 179 | "EGi7cG1+UGAAED87MGx6QG9rMGx6QG6N4Gx6QGAAA=" 180 | ) 181 | } 182 | ] 183 | 184 | 185 | def _decksorted(cards): 186 | return sorted(cards, key=lambda x: x[0]) 187 | 188 | 189 | def _sbsorted(cards): 190 | return sorted(cards, key=lambda x: (x[2], x[0])) 191 | 192 | 193 | def test_empty_deckstring(): 194 | deck = deckstrings.Deck() 195 | deck.heroes = [0] 196 | assert deck.as_deckstring == "AAEAAQAAAAAA" 197 | 198 | 199 | def test_decode_pre_sideboard_deckstring(): 200 | deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING_PRE_SIDEBOARD) 201 | assert deck.get_dbf_id_list() == _decksorted(TEST_DECKSTRING_CARDLIST) 202 | assert deck.get_sideboard_dbf_id_list() == [] 203 | assert deck.format == FormatType.FT_WILD 204 | assert deck.heroes == [31] # Rexxar 205 | 206 | 207 | def test_decode_deckstring(): 208 | deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING) 209 | assert deck.get_dbf_id_list() == _decksorted(TEST_DECKSTRING_CARDLIST) 210 | assert deck.get_sideboard_dbf_id_list() == [] 211 | assert deck.format == FormatType.FT_WILD 212 | assert deck.heroes == [31] # Rexxar 213 | 214 | 215 | def test_encode_deckstring(): 216 | deck = deckstrings.Deck() 217 | deck.cards = _decksorted(TEST_DECKSTRING_CARDLIST) 218 | deck.sideboards = [] 219 | deck.format = FormatType.FT_WILD 220 | deck.heroes = [31] 221 | assert deck.as_deckstring == TEST_DECKSTRING 222 | 223 | 224 | def test_reencode_deckstring(): 225 | deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING) 226 | assert deck.as_deckstring == TEST_DECKSTRING 227 | 228 | 229 | def test_decode_sideboard_deckstring(): 230 | deck = deckstrings.Deck.from_deckstring(TEST_SIDEBOARD_DECKSTRING) 231 | assert deck.get_dbf_id_list() == _decksorted(TEST_SIDEBOARD_DECKSTRING_CARDLIST) 232 | assert deck.sideboards == _decksorted(TEST_SIDEBOARD_DECKSTRING_SIDEBOARD) 233 | assert deck.format == FormatType.FT_WILD 234 | assert deck.heroes == [101648] # Hedanis 235 | 236 | 237 | def test_encode_sideboard_deckstring(): 238 | deck = deckstrings.Deck() 239 | deck.cards = _decksorted(TEST_SIDEBOARD_DECKSTRING_CARDLIST) 240 | deck.sideboards = _decksorted(TEST_SIDEBOARD_DECKSTRING_SIDEBOARD) 241 | deck.format = FormatType.FT_WILD 242 | deck.heroes = [101648] 243 | assert deck.as_deckstring == TEST_SIDEBOARD_DECKSTRING 244 | 245 | 246 | def test_reencode_sideboard_deckstring(): 247 | deck = deckstrings.Deck.from_deckstring(TEST_SIDEBOARD_DECKSTRING) 248 | assert deck.as_deckstring == TEST_SIDEBOARD_DECKSTRING 249 | 250 | 251 | def test_encode_canonical_deckstring(): 252 | deck = deckstrings.Deck() 253 | deck.cards = [ 254 | (6, 1), 255 | (4, 1), 256 | (2, 2), 257 | (7, 2), 258 | (1, 1), 259 | (5, 2), 260 | (9, 3), 261 | (3, 3), 262 | ] 263 | deck.sideboards = [ 264 | (8, 1, 3), 265 | (10, 1, 2), 266 | (1, 1, 3), 267 | ] 268 | deck.heroes = [31] 269 | deck.format = FormatType.FT_WILD 270 | assert deck.as_deckstring == "AAEBAR8DAQQGAwIFBwIDAwkDAQMKAgEDCAMAAA==" 271 | 272 | 273 | def test_decode_canonical_deckstring(): 274 | deck = deckstrings.Deck.from_deckstring("AAEBAx8hHgMBBAYDAgUHAgMDCQMBAwoCAQMIAwAA") 275 | assert deck.cards == [ 276 | (1, 1), 277 | (2, 2), 278 | (3, 3), 279 | (4, 1), 280 | (5, 2), 281 | (6, 1), 282 | (7, 2), 283 | (9, 3), 284 | ] 285 | assert deck.sideboards == [ 286 | (10, 1, 2), 287 | (1, 1, 3), 288 | (8, 1, 3), 289 | ] 290 | deck.heroes = [30, 31, 33] 291 | 292 | 293 | def test_deckstrings_regression(): 294 | for deckdata in DECKSTRING_TEST_DATA: 295 | sideboards = deckdata.get("sideboards", []) 296 | 297 | # Encode tests 298 | deck = deckstrings.Deck() 299 | deck.cards = deckdata["cards"] 300 | deck.sideboards = sideboards 301 | deck.heroes = deckdata["heroes"] 302 | deck.format = deckdata["format"] 303 | 304 | assert deck.as_deckstring == deckdata["deckstring"] 305 | 306 | # Decode tests 307 | deck = deckstrings.Deck.from_deckstring(deckdata["deckstring"]) 308 | assert deck.cards == _decksorted(deckdata["cards"]) 309 | assert deck.sideboards == _sbsorted(sideboards) 310 | assert deck.heroes == sorted(deckdata["heroes"]) 311 | assert deck.format == deckdata["format"] 312 | -------------------------------------------------------------------------------- /tests/test_entities.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from hearthstone.entities import Card, Game, Player 4 | from hearthstone.enums import CardSet, CardType, GameTag, Step, Zone 5 | 6 | 7 | class TestGame: 8 | def test_find_entity_by_id(self): 9 | game = Game(1) 10 | game.register_entity(game) 11 | 12 | assert game.find_entity_by_id(1) is game 13 | assert game.find_entity_by_id(2) is None 14 | 15 | 16 | class TestPlayer: 17 | @pytest.fixture 18 | def game(self): 19 | game = Game(1) 20 | game.register_entity(game) 21 | return game 22 | 23 | @pytest.fixture 24 | def player(self, game): 25 | player = Player(2, 1, 0, 0, "Test Player") 26 | game.register_entity(player) 27 | return player 28 | 29 | def test_starting_hero_does_not_exist(self, player): 30 | assert player.starting_hero is None 31 | 32 | def test_starting_hero_from_initial_hero_entity_id(self, game, player): 33 | hero = Card(4, "HERO_02") 34 | game.register_entity(hero) 35 | player.initial_hero_entity_id = hero.id 36 | 37 | assert player.starting_hero == hero 38 | 39 | def test_starting_hero_from_hero_entity(self, game, player): 40 | hero = Card(4, "HERO_02") 41 | game.register_entity(hero) 42 | hero.tags.update({ 43 | GameTag.CARDTYPE: CardType.HERO, 44 | GameTag.CONTROLLER: player.player_id, 45 | }) 46 | 47 | assert player.starting_hero == hero 48 | 49 | def test_starting_hero_maestra(self, game, player): 50 | # Set by the exporter 51 | player.initial_hero_entity_id = 3 52 | 53 | # Create original hero 54 | fake_hero = Card(3, "HERO_07") 55 | fake_hero.tags = { 56 | GameTag.CONTROLLER: 1, 57 | GameTag.CARDTYPE: CardType.HERO, 58 | } 59 | game.register_entity(fake_hero) 60 | 61 | # At this point, the starting hero is the fake one (but we don't know that yet!) 62 | assert player.starting_hero == fake_hero 63 | 64 | # Start the game 65 | game.tag_change(GameTag.STEP, Step.MAIN_READY) 66 | 67 | # ...we play a Rogue card, which creates the real hero: 68 | real_hero = Card(4, "HERO_03") 69 | real_hero.tags = { 70 | GameTag.CONTROLLER: 1, 71 | GameTag.CARDTYPE: CardType.HERO, 72 | GameTag.CREATOR_DBID: 64674, 73 | } 74 | game.register_entity(real_hero) 75 | 76 | # At this point, we should have a new starting_hero 77 | assert player.starting_hero == real_hero 78 | 79 | def test_initial_deck(self, game, player): 80 | WISP = "CS2_231" 81 | 82 | wisp = Card(5, None) 83 | wisp.tags.update({ 84 | GameTag.ZONE: Zone.DECK, 85 | }) 86 | game.register_entity(wisp) 87 | wisp.reveal(WISP, { 88 | GameTag.CARDTYPE: CardType.MINION, 89 | GameTag.CONTROLLER: player.player_id, 90 | }) 91 | 92 | assert list(player.initial_deck) == [wisp] 93 | 94 | def test_initial_deck_unknown(self, game, player): 95 | WISP = "CS2_231" 96 | 97 | wisp = Card(5, None) 98 | wisp.tags.update({ 99 | GameTag.ZONE: Zone.DECK, 100 | GameTag.CONTROLLER: player.player_id, 101 | }) 102 | game.register_entity(wisp) 103 | wisp.reveal(WISP, { 104 | GameTag.CARDTYPE: CardType.MINION, 105 | }) 106 | 107 | hidden = Card(6, None) 108 | hidden.tags.update({ 109 | GameTag.ZONE: Zone.DECK, 110 | GameTag.CONTROLLER: player.player_id, 111 | }) 112 | game.register_entity(hidden) 113 | 114 | assert list(player.initial_deck) == [wisp, hidden] 115 | 116 | def test_initial_deck_with_souleathers_scythe(self, game, player): 117 | wisp = Card(5, None) 118 | wisp.tags.update({ 119 | GameTag.ZONE: Zone.GRAVEYARD, 120 | GameTag.CONTROLLER: player.player_id, 121 | }) 122 | game.register_entity(wisp) 123 | 124 | scythe = Card(6, None) 125 | scythe.tags.update({ 126 | GameTag.ZONE: Zone.DECK, 127 | GameTag.CONTROLLER: player.player_id, 128 | }) 129 | game.register_entity(scythe) 130 | 131 | assert list(player.initial_deck) == [wisp, scythe] 132 | 133 | def test_known_starting_deck_list(self, game, player): 134 | WISP = "CS2_231" 135 | 136 | wisp = Card(5, None) 137 | wisp.tags.update({ 138 | GameTag.ZONE: Zone.DECK, 139 | }) 140 | game.register_entity(wisp) 141 | wisp.reveal(WISP, { 142 | GameTag.CARDTYPE: CardType.MINION, 143 | GameTag.CONTROLLER: player.player_id, 144 | }) 145 | 146 | assert player.known_starting_deck_list == [WISP] 147 | 148 | def test_known_starting_deck_list_duplicates(self, game, player): 149 | WISP = "CS2_231" 150 | 151 | wisp1 = Card(5, None) 152 | wisp1.tags.update({ 153 | GameTag.ZONE: Zone.DECK, 154 | }) 155 | game.register_entity(wisp1) 156 | wisp1.reveal(WISP, { 157 | GameTag.CARDTYPE: CardType.MINION, 158 | GameTag.CONTROLLER: player.player_id, 159 | }) 160 | 161 | wisp2 = Card(5, None) 162 | wisp2.tags.update({ 163 | GameTag.ZONE: Zone.DECK, 164 | }) 165 | game.register_entity(wisp2) 166 | wisp2.reveal(WISP, { 167 | GameTag.CARDTYPE: CardType.MINION, 168 | GameTag.CONTROLLER: player.player_id, 169 | }) 170 | 171 | assert player.known_starting_deck_list == [WISP, WISP] 172 | 173 | def test_known_starting_deck_list_with_zerus(self, game, player): 174 | ZERUS = "OG_123" 175 | ZERUS_DBF = 38475 176 | WISP = "CS2_231" 177 | 178 | zerus = Card(5, None) 179 | zerus.tags.update({ 180 | GameTag.ZONE: Zone.DECK, 181 | }) 182 | game.register_entity(zerus) 183 | zerus.reveal(WISP, { 184 | GameTag.CARDTYPE: CardType.MINION, 185 | GameTag.CONTROLLER: player.player_id, 186 | GameTag.TRANSFORMED_FROM_CARD: ZERUS_DBF, 187 | }) 188 | 189 | assert player.known_starting_deck_list == [ZERUS] 190 | 191 | def test_known_starting_deck_list_with_unidentified_cards(self, game, player): 192 | UNIDENTIFIED_CONTRACT = "DAL_366" 193 | RECRUITMENT_CONTRACT = "DAL_366t2" 194 | 195 | contract = Card(5, None) 196 | contract.tags.update({ 197 | GameTag.ZONE: Zone.DECK, 198 | }) 199 | game.register_entity(contract) 200 | contract.reveal(RECRUITMENT_CONTRACT, { 201 | GameTag.CARDTYPE: CardType.SPELL, 202 | GameTag.CONTROLLER: player.player_id, 203 | }) 204 | 205 | assert player.known_starting_deck_list == [UNIDENTIFIED_CONTRACT] 206 | 207 | def test_known_starting_deck_list_with_galakrond(self, game, player): 208 | GALAKROND = "DRG_600" 209 | GALAKROND_UPGRADE_1 = "DRG_600t2" 210 | GALAKROND_UPGRADE_2 = "DRG_600t3" 211 | 212 | galakrond = Card(13, None) 213 | galakrond.tags.update({ 214 | GameTag.ZONE: Zone.DECK, 215 | }) 216 | game.register_entity(galakrond) 217 | 218 | galakrond.reveal(GALAKROND, { 219 | GameTag.CARDTYPE: CardType.HERO, 220 | GameTag.CONTROLLER: player.player_id, 221 | }) 222 | galakrond.change(GALAKROND_UPGRADE_1, {}) 223 | galakrond.hide() 224 | 225 | assert player.known_starting_deck_list == [GALAKROND], \ 226 | "Galakrond should be known after it was upgraded, even if not played" 227 | 228 | galakrond.reveal(GALAKROND_UPGRADE_1, { 229 | GameTag.CARDTYPE: CardType.HERO, 230 | GameTag.CONTROLLER: player.player_id, 231 | }) 232 | galakrond.change(GALAKROND_UPGRADE_2, {}) 233 | galakrond.hide() 234 | 235 | galakrond.tags.update({ 236 | GameTag.ZONE: Zone.HAND, 237 | }) 238 | galakrond.reveal(GALAKROND_UPGRADE_2, { 239 | GameTag.CARDTYPE: CardType.HERO, 240 | GameTag.CONTROLLER: player.player_id, 241 | }) 242 | 243 | assert player.known_starting_deck_list == [GALAKROND] 244 | 245 | def test_known_starting_deck_list_with_maestra(self, game, player): 246 | MAESTRA = "SW_050" 247 | 248 | game.tag_change(GameTag.STEP, Step.MAIN_READY) 249 | 250 | real_hero = Card(4, "HERO_03") 251 | real_hero.tags = { 252 | GameTag.CONTROLLER: 1, 253 | GameTag.CARDTYPE: CardType.HERO, 254 | GameTag.CREATOR_DBID: 64674, 255 | } 256 | game.register_entity(real_hero) 257 | 258 | assert player.known_starting_deck_list == [MAESTRA] 259 | 260 | def test_known_starting_deck_list_with_souleaters_scythe(self, game, player): 261 | WISP = "CS2_231" 262 | SOULEATERS_SCYTHE = "RLK_214" 263 | BOUND_SOUL = "RLK_214t" 264 | 265 | wisp = Card(5, WISP) 266 | wisp.tags.update({ 267 | GameTag.ZONE: Zone.GRAVEYARD, 268 | GameTag.CONTROLLER: player.player_id, 269 | }) 270 | game.register_entity(wisp) 271 | 272 | scythe = Card(6, None) 273 | scythe.tags.update({ 274 | GameTag.ZONE: Zone.DECK, 275 | GameTag.CONTROLLER: player.player_id, 276 | }) 277 | game.register_entity(scythe) 278 | 279 | game.tag_change(GameTag.NEXT_STEP, Step.MAIN_READY) 280 | 281 | # Start of game: create souls 282 | game.tag_change(GameTag.NEXT_STEP, Step.MAIN_READY) 283 | 284 | soul = Card(7, None) 285 | soul.tags.update({ 286 | GameTag.ZONE: Zone.DECK, 287 | GameTag.CONTROLLER: player.player_id, 288 | }) 289 | game.register_entity(scythe) 290 | soul.reveal(BOUND_SOUL, {}) 291 | soul.hide() 292 | 293 | game.tag_change(GameTag.STEP, Step.MAIN_READY) 294 | 295 | # Draw the Scythe 296 | scythe.reveal(SOULEATERS_SCYTHE, { 297 | GameTag.CARDTYPE: CardType.SPELL, 298 | GameTag.CONTROLLER: player.player_id, 299 | GameTag.ZONE: Zone.HAND, 300 | }) 301 | 302 | # Draw the Soul 303 | soul.reveal(BOUND_SOUL, { 304 | GameTag.CARDTYPE: CardType.SPELL, 305 | GameTag.CONTROLLER: player.player_id, 306 | GameTag.ZONE: Zone.HAND, 307 | }) 308 | 309 | assert player.known_starting_deck_list == [WISP, SOULEATERS_SCYTHE] 310 | 311 | def test_known_starting_deck_list_with_tourist(self, game, player): 312 | HAMM = "VAC_340" 313 | TOURIST_VFX_ENCHANTMENT = "VAC_422e" 314 | 315 | tourist = Card(4, None) 316 | tourist.tags.update({ 317 | GameTag.ZONE: Zone.DECK, 318 | GameTag.CONTROLLER: player.player_id, 319 | }) 320 | game.register_entity(tourist) 321 | 322 | vfx = Card(5, None) 323 | vfx.tags.update({ 324 | GameTag.ZONE: Zone.SETASIDE, 325 | GameTag.CONTROLLER: player.player_id, 326 | }) 327 | game.register_entity(vfx) 328 | vfx.reveal(TOURIST_VFX_ENCHANTMENT, { 329 | GameTag.CARDTYPE: CardType.ENCHANTMENT, 330 | GameTag.ATTACHED: player.id, 331 | GameTag.CREATOR: tourist.id, 332 | }) 333 | 334 | # At some point we play an out-of-class card, and a fake tourist is shown 335 | fake_tourist = Card(6, HAMM) 336 | fake_tourist.tags.update({ 337 | GameTag.CONTROLLER: player.player_id, 338 | GameTag.CREATOR: vfx.id, 339 | GameTag.ZONE: Zone.REMOVEDFROMGAME, 340 | GameTag.TOURIST: 2, 341 | GameTag.DRUID_TOURIST: 1, 342 | }) 343 | game.register_entity(fake_tourist) 344 | 345 | assert player.known_starting_deck_list == [HAMM] 346 | 347 | tourist.reveal(HAMM, {}) 348 | 349 | assert player.known_starting_deck_list == [HAMM] 350 | 351 | 352 | class TestCard: 353 | def test_card(self): 354 | card1 = Card(4, "EX1_001") 355 | # The following should be instant. 356 | # If this test hangs, something's wrong in the caching mechanism... 357 | for i in range(1000): 358 | assert card1.base_tags.get(GameTag.HEALTH, 0) == 2 359 | 360 | def test_change_entity(self): 361 | card = Card(4, "EX1_001") 362 | assert card.card_id == "EX1_001" 363 | assert card.initial_card_id == "EX1_001" 364 | assert card.is_original_entity 365 | 366 | card.change("NEW1_030", {}) 367 | assert card.card_id == "NEW1_030" 368 | assert card.initial_card_id == "EX1_001" 369 | assert not card.is_original_entity 370 | 371 | weapon = Card(4, None) 372 | assert not weapon.initial_card_id 373 | assert weapon.is_original_entity 374 | 375 | weapon.reveal("CS2_091", {GameTag.TRANSFORMED_FROM_CARD: 41420}) 376 | assert weapon.card_id == "CS2_091" 377 | assert weapon.initial_card_id == "UNG_929" 378 | assert not weapon.is_original_entity 379 | 380 | def test_can_be_in_deck(self): 381 | card = Card(31, "HERO_05") 382 | card.tags.update({ 383 | GameTag.CARD_SET: CardSet.HERO_SKINS, 384 | }) 385 | assert card.can_be_in_deck 386 | 387 | def test_archthief_rafaam(self): 388 | card = Card(4, None) 389 | assert not card.initial_card_id 390 | assert card.is_original_entity 391 | 392 | card.reveal("CS2_091", { 393 | GameTag.CREATOR_DBID: 52119 394 | }) 395 | assert card.card_id == "CS2_091" 396 | assert not card.initial_card_id 397 | assert not card.is_original_entity 398 | 399 | card.change("EX1_001", {}) 400 | assert card.card_id == "EX1_001" 401 | assert not card.initial_card_id 402 | assert not card.is_original_entity 403 | 404 | def test_unidentified_contract(self): 405 | card = Card(4, None) 406 | assert not card.initial_card_id 407 | assert card.is_original_entity 408 | 409 | card.reveal("DAL_366", {}) 410 | assert card.card_id == "DAL_366" 411 | assert card.initial_card_id == "DAL_366" 412 | 413 | card.change("DAL_366t3", {}) 414 | assert card.card_id == "DAL_366t3" 415 | assert card.initial_card_id == "DAL_366" 416 | 417 | def test_shifter_zerus(self): 418 | card = Card(4, None) 419 | assert not card.initial_card_id 420 | assert card.is_original_entity 421 | 422 | card.reveal("GIL_650", { 423 | GameTag.TRANSFORMED_FROM_CARD: 38475 424 | }) 425 | assert card.card_id == "GIL_650" 426 | assert card.initial_card_id == "OG_123" 427 | 428 | def test_swift_messenger(self): 429 | card = Card(4, None) 430 | assert not card.initial_card_id 431 | assert card.is_original_entity 432 | 433 | card.reveal("GIL_528t", {}) 434 | assert card.card_id == "GIL_528t" 435 | assert card.initial_card_id == "GIL_528t" 436 | 437 | def test_invalid_transformed_from_card(self): 438 | card = Card(4, None) 439 | card.reveal("EX1_001", {GameTag.TRANSFORMED_FROM_CARD: 0}) 440 | assert card.initial_card_id == "EX1_001" 441 | -------------------------------------------------------------------------------- /tests/test_enums.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from hearthstone import enums 4 | from hearthstone.enums import CardClass, Locale, get_localized_name 5 | 6 | 7 | def test_zodiac_dates(): 8 | assert enums.ZodiacYear.as_of_date(datetime(2014, 1, 1)) == enums.ZodiacYear.PRE_STANDARD 9 | assert enums.ZodiacYear.as_of_date(datetime(2016, 1, 1)) == enums.ZodiacYear.PRE_STANDARD 10 | assert enums.ZodiacYear.as_of_date(datetime(2016, 6, 1)) == enums.ZodiacYear.KRAKEN 11 | assert enums.ZodiacYear.as_of_date(datetime(2017, 1, 1)) == enums.ZodiacYear.KRAKEN 12 | assert enums.ZodiacYear.as_of_date(datetime(2017, 5, 1)) == enums.ZodiacYear.MAMMOTH 13 | assert enums.ZodiacYear.as_of_date(datetime(2018, 5, 1)) == enums.ZodiacYear.RAVEN 14 | 15 | 16 | def test_cardclass(): 17 | playable_cards = [ 18 | enums.CardClass.DEATHKNIGHT, 19 | enums.CardClass.DEMONHUNTER, 20 | enums.CardClass.DRUID, 21 | enums.CardClass.HUNTER, 22 | enums.CardClass.MAGE, 23 | enums.CardClass.PALADIN, 24 | enums.CardClass.PRIEST, 25 | enums.CardClass.ROGUE, 26 | enums.CardClass.SHAMAN, 27 | enums.CardClass.WARLOCK, 28 | enums.CardClass.WARRIOR 29 | ] 30 | 31 | for c in playable_cards: 32 | assert c.is_playable 33 | 34 | for c in enums.CardClass: 35 | if c not in playable_cards: 36 | assert not c.is_playable 37 | 38 | 39 | def test_gametype(): 40 | gt = enums.GameType 41 | bgt = enums.BnetGameType 42 | 43 | assert gt.GT_RANKED.as_bnet(format=enums.FormatType.FT_CLASSIC) == bgt.BGT_RANKED_CLASSIC 44 | assert gt.GT_RANKED.as_bnet(format=enums.FormatType.FT_STANDARD) == bgt.BGT_RANKED_STANDARD 45 | assert gt.GT_RANKED.as_bnet(format=enums.FormatType.FT_WILD) == bgt.BGT_RANKED_WILD 46 | assert gt.GT_CASUAL.as_bnet(format=enums.FormatType.FT_CLASSIC) == bgt.BGT_CASUAL_CLASSIC 47 | assert gt.GT_CASUAL.as_bnet(format=enums.FormatType.FT_STANDARD) == bgt.BGT_CASUAL_STANDARD 48 | assert gt.GT_CASUAL.as_bnet(format=enums.FormatType.FT_WILD) == bgt.BGT_CASUAL_WILD 49 | 50 | assert gt.GT_VS_AI.as_bnet() == bgt.BGT_VS_AI 51 | assert gt.GT_VS_FRIEND.as_bnet() == bgt.BGT_FRIENDS 52 | 53 | assert gt.GT_FSG_BRAWL_VS_FRIEND.is_fireside 54 | assert gt.GT_FSG_BRAWL.is_fireside 55 | assert gt.GT_FSG_BRAWL_1P_VS_AI.is_fireside 56 | assert gt.GT_FSG_BRAWL_2P_COOP.is_fireside 57 | assert not gt.GT_RANKED.is_fireside 58 | 59 | assert gt.GT_TAVERNBRAWL.is_tavern_brawl 60 | assert gt.GT_TB_1P_VS_AI.is_tavern_brawl 61 | assert gt.GT_TB_2P_COOP.is_tavern_brawl 62 | assert not gt.GT_RANKED.is_tavern_brawl 63 | 64 | 65 | class TestCardSet: 66 | def test_name_global(self): 67 | assert enums.CardSet.NAXX.name_global == "GLOBAL_CARD_SET_NAXX" 68 | assert enums.CardSet.THE_SUNKEN_CITY.name_global == "GLOBAL_CARD_SET_TSC" 69 | 70 | 71 | class TestMultiClassGroup: 72 | def test_card_classes(self): 73 | assert enums.MultiClassGroup.GRIMY_GOONS.card_classes == [ 74 | enums.CardClass.HUNTER, 75 | enums.CardClass.WARRIOR, 76 | enums.CardClass.PALADIN, 77 | ] 78 | assert enums.MultiClassGroup.INVALID.card_classes == [] 79 | 80 | 81 | def test_get_localized_name(): 82 | d = { 83 | locale.name: get_localized_name(CardClass.DRUID, locale.name) for locale in Locale 84 | if not locale.unused 85 | } 86 | 87 | assert d == { 88 | "deDE": "Druide", 89 | "enUS": "Druid", 90 | "esES": "Druida", 91 | "esMX": "Druida", 92 | "frFR": "Druide", 93 | "itIT": "Druido", 94 | "jaJP": "ドルイド", 95 | "koKR": "드루이드", 96 | "plPL": "Druid", 97 | "ptBR": "Druida", 98 | "ruRU": "Друид", 99 | "thTH": "ดรูอิด", 100 | "zhCN": "德鲁伊", 101 | "zhTW": "德魯伊" 102 | } 103 | -------------------------------------------------------------------------------- /tests/test_mercenaryxml.py: -------------------------------------------------------------------------------- 1 | from hearthstone import mercenaryxml 2 | from hearthstone.enums import Rarity 3 | 4 | 5 | def test_mercenaryxml_load(): 6 | mercenary_db, _ = mercenaryxml.load() 7 | 8 | assert mercenary_db 9 | 10 | assert mercenary_db[3].name == "Kurtrus Ashfallen" 11 | assert mercenary_db[3].collectible 12 | assert mercenary_db[3].rarity == Rarity.RARE 13 | 14 | assert mercenary_db[231].name == "Toki" 15 | assert not mercenary_db[231].collectible 16 | assert mercenary_db[231].rarity == Rarity.LEGENDARY 17 | -------------------------------------------------------------------------------- /tests/test_stringsfile.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | from hearthstone.stringsfile import load_txt 4 | 5 | 6 | TEST_STRINGS = """TAG TEXT COMMENT AUDIOFILE 7 | VO_ICC09_Saurfang_Male_Orc_CursedBlade_01 Who’s idea was this? 8 | VO_ICC09_Saurfang_Male_Orc_Doomerang_01 Hmm… I gotta get one of those… 9 | 10 | VO_ICC06_Marrowgar_Male_BoneWraith_Intro_01 None may enter the master's sanctum! 11 | VO_ICC06_Marrowgar_Male_BoneWraith_Bonespike_01 The only escape is death!""" # noqa: W291 12 | 13 | 14 | def test_load_blank_line(): 15 | assert load_txt(StringIO(TEST_STRINGS)) == { 16 | "VO_ICC09_Saurfang_Male_Orc_CursedBlade_01": { 17 | "TEXT": "Who’s idea was this?" 18 | }, 19 | "VO_ICC09_Saurfang_Male_Orc_Doomerang_01": { 20 | "TEXT": "Hmm… I gotta get one of those…" 21 | }, 22 | "VO_ICC06_Marrowgar_Male_BoneWraith_Intro_01": { 23 | "TEXT": "None may enter the master's sanctum! " 24 | }, 25 | "VO_ICC06_Marrowgar_Male_BoneWraith_Bonespike_01": { 26 | "TEXT": "The only escape is death!" 27 | } 28 | } 29 | 30 | 31 | def test_handle_null_bytes(): 32 | NULL_BYTE_STRING = """TAG TEXT COMMENT AUDIOFILE 33 | SOME_STRING_KEY There's a bad null byte at the end!\0""" # noqa: W291 34 | 35 | assert load_txt(StringIO(NULL_BYTE_STRING)) == { 36 | "SOME_STRING_KEY": { 37 | "TEXT": "There's a bad null byte at the end!", 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from hearthstone import cardxml 2 | from hearthstone.enums import Race 3 | from hearthstone.utils import CARDRACE_TAG_MAP, UPGRADABLE_CARDS_MAP 4 | 5 | 6 | def test_upgradable_card_map(): 7 | cardid_db, _ = cardxml.load() 8 | 9 | for upgraded, original in UPGRADABLE_CARDS_MAP.items(): 10 | assert cardid_db[original] 11 | assert cardid_db[original].collectible 12 | assert cardid_db[upgraded] 13 | assert not cardid_db[upgraded].collectible 14 | 15 | 16 | def test_race_tag_map(): 17 | for race in Race: 18 | if race != Race.INVALID: 19 | assert race in CARDRACE_TAG_MAP, \ 20 | "%s is missing from utils.CARDRACE_TAG_MAP" % race 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310, flake8, mypy 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONWARNINGS = all 7 | commands = pytest --showlocals {posargs} 8 | deps = 9 | pytest 10 | pytest-mock 11 | requests-mock 12 | 13 | 14 | [testenv:flake8] 15 | skip_install = True 16 | commands = 17 | flake8 18 | deps = 19 | flake8==3.7.7 20 | flake8-isort==2.6.0 21 | flake8-quotes==1.0.0 22 | isort<5 23 | 24 | [testenv:mypy] 25 | commands = 26 | mypy --ignore-missing --install-types hearthstone 27 | mypy --ignore-missing-imports hearthstone 28 | deps = 29 | mypy 30 | types-requests 31 | types-setuptools 32 | 33 | [flake8] 34 | ignore = E117, W191, I201, W504, E731 35 | max-line-length = 92 36 | exclude = .tox, build/ 37 | inline-quotes = double 38 | 39 | [isort] 40 | indent = tab 41 | line_length = 92 42 | lines_after_imports = 2 43 | balanced_wrapping = true 44 | combine_as_imports = true 45 | default_section = THIRDPARTY 46 | known_first_party = hearthstone 47 | multi_line_output = 5 48 | skip = .tox, build/ 49 | --------------------------------------------------------------------------------