"""Download UI art used by https://www.dota2.com/heroes / dota2.com.cn. - Landscape hero cards: Steam CDN `heroes/{key}.png` - Attribute icons: Steam CDN `icons/hero_{strength,agility,...}.png` - Combat stat icons: dota2.com.cn `herostatic/stats/icon_*.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 assets/ui_icons/icon_{damage,armor,...}.png """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import argparse import json from shared.http_utils import fetch_hero_keys, http_bytes from shared.paths import ATTR_ICONS, HERO_PORTRAITS, HEROES_JSON, UI_ICONS # 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", } # Same filenames as https://www.dota2.com.cn/hero/ DetailsStats panel. COMBAT_STAT_ICON_URL = "https://www.dota2.com.cn/herostatic/stats/{name}.png" COMBAT_STAT_ICONS = ( "icon_damage", "icon_attack_time", "icon_attack_range", "icon_projectile_speed", "icon_armor", "icon_magic_resist", "icon_movement_speed", "icon_turn_rate", "icon_vision", ) 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 fetch_combat_stat_icons(*, force: bool) -> tuple[int, int, int]: UI_ICONS.mkdir(parents=True, exist_ok=True) ok = skip = fail = 0 for name in COMBAT_STAT_ICONS: out = UI_ICONS / f"{name}.png" if out.is_file() and not force: skip += 1 continue try: data = http_bytes(COMBAT_STAT_ICON_URL.format(name=name), timeout=30) out.write_bytes(data) ok += 1 print(f" combat {name} ({len(data)} bytes)") except Exception as e: # noqa: BLE001 fail += 1 print(f" FAIL combat {name}: {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) print("combat stat icons...") c_ok, c_skip, c_fail = fetch_combat_stat_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"combat: downloaded={c_ok} skipped={c_skip} failed={c_fail} -> {UI_ICONS}" ) print(f"heroes: downloaded={ok} skipped={skip} failed={fail} -> {HERO_PORTRAITS}") if __name__ == "__main__": main()