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:
voson
2026-07-29 02:11:49 +08:00
co-authored by Cursor
parent 37769580f5
commit 3ec8007077
72 changed files with 20669 additions and 2105 deletions
+84
View File
@@ -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,
}