v0.5.109: hero role toolbar, skill side-by-side layout, taller detail panel.
Move role filters between grid and detail, fix skill video/text layout and detail height jump, unify item dividers, drop OpenDota matchup cross, sync docs and assets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+36
-64
@@ -28,7 +28,6 @@ import urllib.error
|
||||
from shared.grid import ATTR_ORDER, hero_table
|
||||
from shared.hero_tags import TAG_ORDER, tags_for_hero
|
||||
from shared.http_utils import http_bytes
|
||||
from shared.matchup_cross import build_baseline, enrich_hero_matchups_cross
|
||||
from shared.paths import (
|
||||
ABILITY_ICONS,
|
||||
ABILITY_VIDEOS,
|
||||
@@ -38,8 +37,8 @@ from shared.paths import (
|
||||
ITEM_CAT_ICONS,
|
||||
ITEM_ICONS,
|
||||
RANK_ICONS,
|
||||
ROLE_ICONS,
|
||||
ROOT,
|
||||
SHARED_DATA,
|
||||
STREAMER_AVATARS,
|
||||
STREAMER_VIDEOS,
|
||||
TEMPLATES_CDN,
|
||||
@@ -91,7 +90,6 @@ PRO_MATCHES_PATH = DATA / "pro_matches.json"
|
||||
STREAMERS_PATH = DATA / "streamers.json"
|
||||
STRATZ_HERO_META_PATH = DATA / "stratz_hero_meta.json"
|
||||
STRATZ_MATCHUP_TOPS_PATH = DATA / "stratz_matchup_tops.json"
|
||||
OPENDOTA_MATCHUPS_PATH = SHARED_DATA / "matchups.json"
|
||||
_LEGACY_DATA = ROOT / "data"
|
||||
|
||||
|
||||
@@ -428,23 +426,8 @@ def load_stratz_hero_meta() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _load_opendota_matchups() -> dict:
|
||||
path = _first_existing(OPENDOTA_MATCHUPS_PATH, _LEGACY_DATA / "matchups.json")
|
||||
if path is None:
|
||||
return {}
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
|
||||
|
||||
def load_stratz_matchup_tops() -> dict:
|
||||
"""STRATZ vs/with top lists per hero (see fetch_stratz_meta.py). Web only.
|
||||
|
||||
When OpenDota matchups.json is present, each counter/countered row gets a
|
||||
``cross`` quality blob (agree / conflict / weak). Observation only.
|
||||
"""
|
||||
"""STRATZ vs/with top lists per hero (see fetch_stratz_meta.py). Web only."""
|
||||
empty: dict = {
|
||||
"fetched_at": None,
|
||||
"started_at": None,
|
||||
@@ -459,7 +442,6 @@ def load_stratz_matchup_tops() -> dict:
|
||||
},
|
||||
"note": None,
|
||||
"stats": {},
|
||||
"cross_source": None,
|
||||
"by_hero": {},
|
||||
}
|
||||
path = _first_existing(
|
||||
@@ -474,9 +456,8 @@ def load_stratz_matchup_tops() -> dict:
|
||||
if not isinstance(raw, dict):
|
||||
return empty
|
||||
|
||||
by_hero_raw = dict(raw.get("by_hero") or {})
|
||||
scope = raw.get("scope") if isinstance(raw.get("scope"), dict) else empty["scope"]
|
||||
out: dict = {
|
||||
return {
|
||||
"fetched_at": raw.get("fetched_at"),
|
||||
"started_at": raw.get("started_at"),
|
||||
"finished_at": raw.get("finished_at"),
|
||||
@@ -487,50 +468,9 @@ def load_stratz_matchup_tops() -> dict:
|
||||
"scope": scope,
|
||||
"note": raw.get("note"),
|
||||
"stats": dict(raw.get("stats") or {}),
|
||||
"cross_source": None,
|
||||
"by_hero": by_hero_raw,
|
||||
"by_hero": dict(raw.get("by_hero") or {}),
|
||||
}
|
||||
|
||||
odota = _load_opendota_matchups()
|
||||
by_odota = odota.get("by_hero") or {}
|
||||
if not by_odota:
|
||||
out["cross_source"] = {
|
||||
"available": False,
|
||||
"label_zh": "暂无可靠交叉结论",
|
||||
"reason": "missing_opendota_matchups",
|
||||
}
|
||||
return out
|
||||
|
||||
key_to_id = {h["key"]: int(h["id"]) for h in hero_table()}
|
||||
baseline = build_baseline(by_odota)
|
||||
enriched: dict[str, dict] = {}
|
||||
for key, cell in by_hero_raw.items():
|
||||
if not isinstance(cell, dict):
|
||||
continue
|
||||
hid = key_to_id.get(key)
|
||||
if hid is None:
|
||||
enriched[key] = cell
|
||||
continue
|
||||
enriched[key] = enrich_hero_matchups_cross(
|
||||
cell,
|
||||
hero_id=hid,
|
||||
by_odota=by_odota,
|
||||
baseline=baseline,
|
||||
)
|
||||
out["by_hero"] = enriched
|
||||
out["cross_source"] = {
|
||||
"available": True,
|
||||
"source": "opendota",
|
||||
"attribution": odota.get("attribution") or "https://opendota.com",
|
||||
"fetched_at": odota.get("fetched_at"),
|
||||
"label_zh": "OpenDota 独立交叉(基线校正)",
|
||||
"manual_review_note": (
|
||||
"Dota2ProTracker(7k+ / 职业)仅作人工复核「来源分歧」条目的高分段参考,"
|
||||
"不进入自动流水线。"
|
||||
),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def load_hero_abilities() -> dict:
|
||||
"""Slim per-hero abilities / Aghs upgrades / talents for the web hero pane."""
|
||||
@@ -932,6 +872,27 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
self._send(200, fpath.read_bytes(), "image/png")
|
||||
return
|
||||
if path.startswith("/role-icon/"):
|
||||
key = path[len("/role-icon/") :]
|
||||
allowed_role_icons = {
|
||||
"Carry.png",
|
||||
"Support.png",
|
||||
"Pusher.png",
|
||||
"Escape.png",
|
||||
"Nuker.png",
|
||||
"Initiator.png",
|
||||
"Disabler.png",
|
||||
"Durable.png",
|
||||
}
|
||||
if key not in allowed_role_icons:
|
||||
self._json(400, {"error": "bad role icon"})
|
||||
return
|
||||
fpath = ROLE_ICONS / key
|
||||
if not fpath.is_file():
|
||||
self.send_error(404)
|
||||
return
|
||||
self._send(200, fpath.read_bytes(), "image/png")
|
||||
return
|
||||
if path.startswith("/ui-icon/"):
|
||||
key = path[len("/ui-icon/") :]
|
||||
allowed_ui = {
|
||||
@@ -939,6 +900,17 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"dota2_logo.png",
|
||||
"dota2_logo_wordmark.png",
|
||||
"platform_douyin.png",
|
||||
"platform_bilibili.png",
|
||||
# Hero combat stats (dota2.com.cn/herostatic/stats)
|
||||
"icon_damage.png",
|
||||
"icon_attack_time.png",
|
||||
"icon_attack_range.png",
|
||||
"icon_projectile_speed.png",
|
||||
"icon_armor.png",
|
||||
"icon_magic_resist.png",
|
||||
"icon_movement_speed.png",
|
||||
"icon_turn_rate.png",
|
||||
"icon_vision.png",
|
||||
}
|
||||
if key not in allowed_ui:
|
||||
self._json(400, {"error": "bad ui icon"})
|
||||
|
||||
Reference in New Issue
Block a user