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