v0.4.1: OSS skill videos, site version footer, and mail contact.
Host ability demos on Aliyun OSS instead of Pages bundles; add patches-page version label and top-right mailto link; improve spirit bear portrait export. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+93
-2
@@ -33,8 +33,8 @@ import urllib.error
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from common import ABILITY_ICONS, DATA, ITEM_ICONS
|
||||
from http_utils import http_json
|
||||
from common import ABILITY_ICONS, DATA, HERO_PORTRAITS, ITEM_ICONS
|
||||
from http_utils import http_bytes, http_json
|
||||
|
||||
PATCHES_LIST_URL = "https://www.dota2.com/datafeed/patchnoteslist?language=schinese"
|
||||
PATCH_NOTES_URL = "https://www.dota2.com/datafeed/patchnotes?version={version}&language=schinese"
|
||||
@@ -44,6 +44,13 @@ ABILITY_IDS_URL = "https://raw.githubusercontent.com/odota/dotaconstants/master/
|
||||
ABILITIES_URL = "https://raw.githubusercontent.com/odota/dotaconstants/master/build/abilities.json"
|
||||
ITEM_ICON_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/items/{key}.png"
|
||||
ABILITY_ICON_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/abilities/{key}.png"
|
||||
# Hero-card CDN (same as fetch_hero_portraits). Non-hero units may only exist at half res.
|
||||
HERO_CARD_URL = (
|
||||
"https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png"
|
||||
)
|
||||
# Prefer ability art when Valve's hero-card for a unit is tiny (spirit_bear is 128x72).
|
||||
UNIT_PORTRAIT_FALLBACK_ABILITY = {"spirit_bear": "lone_druid_spirit_bear"}
|
||||
PORTRAIT_TARGET_SIZE = (256, 144)
|
||||
|
||||
OUT = DATA / "patches.json"
|
||||
HERO_ABILITIES_PATH = DATA / "hero_abilities.json"
|
||||
@@ -242,6 +249,86 @@ def build_lookup(
|
||||
return {"items": items, "abilities": abilities, "heroes": heroes}
|
||||
|
||||
|
||||
def _png_size(data: bytes) -> tuple[int, int] | None:
|
||||
if len(data) < 24 or data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
return None
|
||||
import struct
|
||||
|
||||
return struct.unpack(">II", data[16:24])
|
||||
|
||||
|
||||
def _cover_resize_png(data: bytes, size: tuple[int, int] = PORTRAIT_TARGET_SIZE) -> bytes:
|
||||
"""Center-crop / scale image bytes to a 16:9 hero-card PNG."""
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
arr = np.frombuffer(data, dtype=np.uint8)
|
||||
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
|
||||
if img is None:
|
||||
raise ValueError("cv2 could not decode image")
|
||||
if img.ndim == 2:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
||||
elif img.shape[2] == 4:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
|
||||
h, w = img.shape[:2]
|
||||
tw, th = size
|
||||
scale = max(tw / w, th / h)
|
||||
nw, nh = max(tw, int(round(w * scale))), max(th, int(round(h * scale)))
|
||||
resized = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_CUBIC)
|
||||
x0 = max(0, (nw - tw) // 2)
|
||||
y0 = max(0, (nh - th) // 2)
|
||||
crop = resized[y0 : y0 + th, x0 : x0 + tw]
|
||||
ok, buf = cv2.imencode(".png", crop)
|
||||
if not ok:
|
||||
raise ValueError("cv2 could not encode png")
|
||||
return buf.tobytes()
|
||||
|
||||
|
||||
def download_unit_portraits(*, delay: float) -> None:
|
||||
"""Fetch portraits for UNIT_NAMES into hero_portraits (not in heroes.json).
|
||||
|
||||
Valve ships some unit cards at half resolution (spirit_bear 128x72). When a
|
||||
fallback ability icon is configured, use that art cover-cropped to 256x144
|
||||
so the patch page matches normal hero cards.
|
||||
"""
|
||||
HERO_PORTRAITS.mkdir(parents=True, exist_ok=True)
|
||||
ok = skip = fail = 0
|
||||
for unit in UNIT_NAMES.values():
|
||||
key = unit["key"]
|
||||
out = HERO_PORTRAITS / f"{key}.png"
|
||||
if out.is_file():
|
||||
dims = _png_size(out.read_bytes())
|
||||
if dims == PORTRAIT_TARGET_SIZE:
|
||||
skip += 1
|
||||
continue
|
||||
try:
|
||||
fb = UNIT_PORTRAIT_FALLBACK_ABILITY.get(key)
|
||||
if fb:
|
||||
raw = http_bytes(ABILITY_ICON_URL.format(key=fb), timeout=30)
|
||||
data = _cover_resize_png(raw)
|
||||
src = f"ability:{fb}"
|
||||
else:
|
||||
raw = http_bytes(HERO_CARD_URL.format(key=key), timeout=30)
|
||||
dims = _png_size(raw)
|
||||
data = (
|
||||
_cover_resize_png(raw)
|
||||
if dims and dims != PORTRAIT_TARGET_SIZE
|
||||
else raw
|
||||
)
|
||||
src = "hero-card"
|
||||
out.write_bytes(data)
|
||||
ok += 1
|
||||
print(f" unit portrait {key} <- {src} ({len(data)} bytes)", flush=True)
|
||||
time.sleep(max(delay, 0.05))
|
||||
except Exception as e: # noqa: BLE001
|
||||
fail += 1
|
||||
print(f" FAIL unit portrait {key}: {e}", flush=True)
|
||||
print(
|
||||
f" unit portraits saved={ok} skipped={skip} fail={fail} -> {HERO_PORTRAITS}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def download_referenced_icons(lookup: dict, *, delay: float) -> None:
|
||||
"""Pull referenced item + ability icons into assets/ (skip existing)."""
|
||||
from http_utils import download_icons
|
||||
@@ -266,6 +353,10 @@ def download_referenced_icons(lookup: dict, *, delay: float) -> None:
|
||||
)
|
||||
print(f" ability icons saved={saved} skipped={skipped} fail={fail}", flush=True)
|
||||
|
||||
# Pseudo-heroes in patchnotes (e.g. 熊灵) are absent from herolist / heroes.json.
|
||||
print("downloading unit portraits for patch lookup ...", flush=True)
|
||||
download_unit_portraits(delay=delay)
|
||||
|
||||
|
||||
def load_existing_details() -> dict[str, dict]:
|
||||
if not OUT.is_file():
|
||||
|
||||
Reference in New Issue
Block a user