"""Download UI art used by https://www.dota2.com/heroes. - Landscape hero cards: Steam CDN `heroes/{key}.png` - Attribute icons: Steam CDN `icons/hero_{strength,agility,...}.png` Separate from `templates/cdn/` face crops used for top-bar matching. Usage: python fetch_hero_portraits.py python fetch_hero_portraits.py --force Writes: assets/hero_portraits/{key}.png assets/attr_icons/{str,agi,int,all}.png """ from __future__ import annotations import argparse import json from common import ATTR_ICONS, HERO_PORTRAITS, HEROES_JSON from http_utils import fetch_hero_keys, http_bytes # wide = face headshots; crop = waist-up 3D renders (usually worse in dense grids). CARD_URL = ( "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png" ) CROP_URL = ( "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/crops/{key}.png" ) ATTR_ICON_URL = ( "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/icons/{name}.png" ) ATTR_FILES = { "str": "hero_strength", "agi": "hero_agility", "int": "hero_intelligence", "all": "hero_universal", } def hero_keys() -> list[str]: if HEROES_JSON.is_file(): rows = json.loads(HEROES_JSON.read_text(encoding="utf-8")) return [h["key"] for h in rows if h.get("key")] return fetch_hero_keys() def fetch_attr_icons(*, force: bool) -> tuple[int, int, int]: ATTR_ICONS.mkdir(parents=True, exist_ok=True) ok = skip = fail = 0 for attr, name in ATTR_FILES.items(): out = ATTR_ICONS / f"{attr}.png" if out.is_file() and not force: skip += 1 continue try: data = http_bytes(ATTR_ICON_URL.format(name=name), timeout=30) out.write_bytes(data) ok += 1 print(f" attr {attr} <- {name} ({len(data)} bytes)") except Exception as e: # noqa: BLE001 fail += 1 print(f" FAIL attr {attr}: {e}") return ok, skip, fail def main() -> None: ap = argparse.ArgumentParser(description="Download dota2.com/heroes UI art") ap.add_argument("--force", action="store_true", help="re-download existing files") ap.add_argument( "--style", choices=("crop", "wide"), default="wide", help="wide = face headshots for draft grid (default); crop = waist-up renders", ) args = ap.parse_args() print("attribute icons...") a_ok, a_skip, a_fail = fetch_attr_icons(force=args.force) HERO_PORTRAITS.mkdir(parents=True, exist_ok=True) keys = hero_keys() ok = skip = fail = 0 url_t = CROP_URL if args.style == "crop" else CARD_URL print(f"hero cards ({args.style})...") for key in keys: out = HERO_PORTRAITS / f"{key}.png" if out.is_file() and not args.force: skip += 1 continue try: data = http_bytes(url_t.format(key=key), timeout=30) out.write_bytes(data) ok += 1 print(f" ok {key} ({len(data)} bytes)") except Exception as e: # noqa: BLE001 fail += 1 print(f" FAIL {key}: {e}") print(f"attrs: downloaded={a_ok} skipped={a_skip} failed={a_fail} -> {ATTR_ICONS}") print(f"heroes: downloaded={ok} skipped={skip} failed={fail} -> {HERO_PORTRAITS}") if __name__ == "__main__": main()