v0.5.59: item counter evidence for fears, catch up Web features to site version.
Ship OpenDota counter-stats reordering for feared items, finalize SITE_VERSION/docs for rankings/streamers/trends/matches/mechanics and draft archetypes, and ignore regenerable Web data caches. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ Usage:
|
||||
python fetch_hero_abilities.py --delay 0.2
|
||||
python fetch_hero_abilities.py --icons # also cache ability PNGs
|
||||
python fetch_hero_abilities.py --icons-only # icons from existing JSON
|
||||
python fetch_hero_abilities.py --tags-only # recompute mechanic tags only
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,6 +28,11 @@ from grid import hero_table
|
||||
from hero_tags import ILLUSION_KEYS
|
||||
from http_utils import download_icons, http_json
|
||||
from loc_format import format_loc, strip_html
|
||||
from mechanic_tags import (
|
||||
QUERY_MECHANIC_ORDER,
|
||||
apply_mechanic_tags,
|
||||
merge_tag_overrides,
|
||||
)
|
||||
|
||||
HERODATA_URL = (
|
||||
"https://www.dota2.com/datafeed/herodata?language=schinese&hero_id={hero_id}"
|
||||
@@ -38,6 +44,7 @@ ABILITY_ICON_URL = (
|
||||
"https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/abilities/{key}.png"
|
||||
)
|
||||
OUT = DATA / "hero_abilities.json"
|
||||
ABILITY_TAG_OVERRIDES = DATA / "ability_tag_overrides.json"
|
||||
DESC_MAX = 600
|
||||
UPGRADE_MAX = 500
|
||||
TALENT_LEVELS = (10, 10, 15, 15, 20, 20, 25, 25)
|
||||
@@ -45,6 +52,50 @@ TALENT_LEVELS = (10, 10, 15, 15, 20, 20, 25, 25)
|
||||
SKIP_ICON_KEYS = frozenset({"innate", "talent_tree"})
|
||||
|
||||
|
||||
def _load_ability_tag_overrides() -> dict[str, dict]:
|
||||
if not ABILITY_TAG_OVERRIDES.is_file():
|
||||
return {}
|
||||
try:
|
||||
raw = json.loads(ABILITY_TAG_OVERRIDES.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
items = raw.get("abilities") or {}
|
||||
return {str(k): v for k, v in items.items() if isinstance(v, dict)}
|
||||
|
||||
|
||||
def ability_mechanic_tags(ab: dict, overrides: dict[str, dict] | None = None) -> list[str]:
|
||||
"""Compute applies-* mechanic tags for one ability row."""
|
||||
key = str(ab.get("key") or "")
|
||||
blob = " ".join(
|
||||
[
|
||||
str(ab.get("name_loc") or ""),
|
||||
str(ab.get("desc_loc") or ""),
|
||||
str(ab.get("shard_loc") or ""),
|
||||
str(ab.get("scepter_loc") or ""),
|
||||
]
|
||||
)
|
||||
auto = apply_mechanic_tags(blob, key=key)
|
||||
ov = (overrides or {}).get(key)
|
||||
return merge_tag_overrides(auto, ov, QUERY_MECHANIC_ORDER)
|
||||
|
||||
|
||||
def retag_abilities(by_hero: dict, overrides: dict[str, dict] | None = None) -> int:
|
||||
"""Write ``tags`` onto every ability; return number of abilities with tags."""
|
||||
ov = overrides if overrides is not None else _load_ability_tag_overrides()
|
||||
tagged = 0
|
||||
for cell in by_hero.values():
|
||||
if not isinstance(cell, dict):
|
||||
continue
|
||||
for ab in cell.get("abilities") or []:
|
||||
if not isinstance(ab, dict) or not ab.get("key"):
|
||||
continue
|
||||
tags = ability_mechanic_tags(ab, ov)
|
||||
ab["tags"] = tags
|
||||
if tags:
|
||||
tagged += 1
|
||||
return tagged
|
||||
|
||||
|
||||
def _load_talent_overrides() -> dict[str, str]:
|
||||
"""Load manual name_loc overrides for talents Valve omits bonus data for."""
|
||||
p = DATA / "talent_overrides.json"
|
||||
@@ -64,6 +115,7 @@ def _load_talent_overrides() -> dict[str, str]:
|
||||
|
||||
|
||||
TALENT_OVERRIDES = _load_talent_overrides()
|
||||
ABILITY_TAG_OVERRIDES_MAP = _load_ability_tag_overrides()
|
||||
|
||||
# Valve dispellable int → label
|
||||
DISPEL_MAP = {
|
||||
@@ -452,6 +504,7 @@ def ability_row(raw: dict, odota: dict[str, dict]) -> dict | None:
|
||||
"mana_cost": mana_cost,
|
||||
"specials": special_rows(sv),
|
||||
"lore_loc": strip_html(raw.get("lore_loc") or "").strip(),
|
||||
"tags": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -580,11 +633,38 @@ def main() -> None:
|
||||
action="store_true",
|
||||
help="Re-download ability icons even when a local PNG already exists",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--tags-only",
|
||||
action="store_true",
|
||||
help="Recompute ability mechanic tags from cached JSON without refetching",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
heroes = hero_table()
|
||||
heroes_by_key = {h["key"]: h for h in heroes}
|
||||
|
||||
if args.tags_only:
|
||||
if not args.out.is_file():
|
||||
raise SystemExit(f"missing {args.out}; run without --tags-only first")
|
||||
prev = json.loads(args.out.read_text(encoding="utf-8"))
|
||||
by_hero = dict(prev.get("by_hero") or {})
|
||||
overrides = _load_ability_tag_overrides()
|
||||
tagged = retag_abilities(by_hero, overrides)
|
||||
meta = dict(prev.get("meta") or {})
|
||||
meta["fetched_at"] = datetime.now(timezone.utc).isoformat()
|
||||
meta["tags_only"] = True
|
||||
meta["mechanic_tag_order"] = list(QUERY_MECHANIC_ORDER)
|
||||
payload = {"meta": meta, "by_hero": by_hero}
|
||||
args.out.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(
|
||||
f"retag done: {len(by_hero)} heroes, {tagged} abilities with tags -> {args.out}",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
|
||||
if args.icons_only:
|
||||
if not args.out.is_file():
|
||||
raise SystemExit(f"missing {args.out}; run without --icons-only first")
|
||||
@@ -693,6 +773,7 @@ def main() -> None:
|
||||
continue
|
||||
ab = ability_row(row, odota)
|
||||
if ab:
|
||||
ab["tags"] = ability_mechanic_tags(ab, ABILITY_TAG_OVERRIDES_MAP)
|
||||
abilities.append(ab)
|
||||
|
||||
talents = talent_rows(data.get("talents"), odota, abs_raw)
|
||||
@@ -742,12 +823,15 @@ def main() -> None:
|
||||
)
|
||||
time.sleep(args.delay)
|
||||
|
||||
tagged = retag_abilities(by_hero, ABILITY_TAG_OVERRIDES_MAP)
|
||||
payload = {
|
||||
"meta": {
|
||||
"source": "valve+opendota",
|
||||
"attribution": "https://www.dota2.com ; https://www.opendota.com",
|
||||
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
||||
"heroes": len(by_hero),
|
||||
"mechanic_tag_order": list(QUERY_MECHANIC_ORDER),
|
||||
"abilities_with_tags": tagged,
|
||||
},
|
||||
"by_hero": by_hero,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user