├── 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 | [](https://github.com/HearthSim/python-hearthstone/actions/workflows/ci.yml)
4 | [](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 |
--------------------------------------------------------------------------------