v0.2.0: relations preview, item shop, abilities, overlay recommend, GSI enhancements
- Add relations/item/abilities preview (serve_relations.py + web/relations/) - Add fetch scripts: hero_items, item_shop, items_meta, hero_abilities, ability_videos, patches, stratz, matchups, portraits - Add overlay.py (role tags + Top-3 cyan marks), recommend.py - Add http_utils.py, loc_format.py, hero_tags.py, item_fears.py - GSI: full payload JSONL dump, foreground window detection - Drop real template library; CDN-only matching - Update docs: CHANGELOG 0.2.0, DESIGN config table, AGENTS module table - .gitignore: exclude large regenerable assets (icons/portraits/videos)
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Export the relations preview as a static site (no server needed).
|
||||
|
||||
Usage:
|
||||
python export_relations_site.py [--out dist/relations] [--with-videos]
|
||||
|
||||
Copies web/relations/ + a snapshot of the /api/data payload (data.json) +
|
||||
the referenced image assets into one directory, ready for any static host
|
||||
(GitHub Pages, Cloudflare Pages, nginx, ...).
|
||||
|
||||
Notes:
|
||||
- Only already-cached assets are exported. For full ability-icon coverage
|
||||
run `python fetch_hero_abilities.py --icons-only` first.
|
||||
- For patch-notes names/icons on the 版本 page, run `python fetch_patches.py`
|
||||
first (downloads referenced item + ability icons into assets/).
|
||||
- Ability videos (several GB locally) are skipped unless --with-videos is
|
||||
given; the UI degrades quietly when a video is missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from common import (
|
||||
ABILITY_ICONS,
|
||||
ABILITY_VIDEOS,
|
||||
ATTR_ICONS,
|
||||
HERO_PORTRAITS,
|
||||
ITEM_CAT_ICONS,
|
||||
ITEM_ICONS,
|
||||
ROOT,
|
||||
TEMPLATES_CDN,
|
||||
UI_ICONS,
|
||||
)
|
||||
from serve_relations import WEB_DIR, build_payload
|
||||
|
||||
|
||||
def copy_glob(src: Path, dst: Path, pattern: str = "*.png") -> int:
|
||||
if not src.is_dir():
|
||||
return 0
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
n = 0
|
||||
for f in sorted(src.glob(pattern)):
|
||||
if f.is_file():
|
||||
shutil.copy2(f, dst / f.name)
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Export relations preview as a static site"
|
||||
)
|
||||
ap.add_argument("--out", default=str(ROOT / "dist" / "relations"))
|
||||
ap.add_argument(
|
||||
"--with-videos",
|
||||
action="store_true",
|
||||
help="also copy assets/ability_videos (several GB)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
out = Path(args.out).resolve()
|
||||
if out == ROOT.resolve() or out.parent == out:
|
||||
raise SystemExit(f"refusing unsafe --out: {out}")
|
||||
if out.exists():
|
||||
shutil.rmtree(out)
|
||||
out.mkdir(parents=True)
|
||||
|
||||
# Frontend (uses relative paths — works from any sub-path).
|
||||
for name in ("index.html", "app.js", "style.css"):
|
||||
shutil.copy2(WEB_DIR / name, out / name)
|
||||
|
||||
# Data snapshot (same payload as serve_relations /api/data).
|
||||
payload = build_payload()
|
||||
(out / "data.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Assets.
|
||||
counts = {
|
||||
"attr": copy_glob(ATTR_ICONS, out / "attr"),
|
||||
"item": copy_glob(ITEM_ICONS, out / "item"),
|
||||
"item-cat": copy_glob(ITEM_CAT_ICONS, out / "item-cat", "itemcat_*.png"),
|
||||
"ability": copy_glob(ABILITY_ICONS, out / "ability"),
|
||||
"ui-icon": copy_glob(UI_ICONS, out / "ui-icon"),
|
||||
}
|
||||
|
||||
# Portraits: official wide cards, fall back to CDN match templates.
|
||||
portrait_dst = out / "portrait"
|
||||
portrait_dst.mkdir(exist_ok=True)
|
||||
n_portrait = 0
|
||||
for hero in payload.get("heroes") or []:
|
||||
key = hero.get("key")
|
||||
if not key:
|
||||
continue
|
||||
src = HERO_PORTRAITS / f"{key}.png"
|
||||
if not src.is_file():
|
||||
src = TEMPLATES_CDN / f"{key}.png"
|
||||
if src.is_file():
|
||||
shutil.copy2(src, portrait_dst / f"{key}.png")
|
||||
n_portrait += 1
|
||||
counts["portrait"] = n_portrait
|
||||
|
||||
n_videos = 0
|
||||
if args.with_videos and ABILITY_VIDEOS.is_dir():
|
||||
shutil.copytree(ABILITY_VIDEOS, out / "ability-video")
|
||||
n_videos = sum(1 for f in (out / "ability-video").rglob("*") if f.is_file())
|
||||
|
||||
total = sum(f.stat().st_size for f in out.rglob("*") if f.is_file())
|
||||
print(f"exported static site -> {out}")
|
||||
for name, n in counts.items():
|
||||
print(f" {name}/: {n} files")
|
||||
if args.with_videos:
|
||||
print(f" ability-video/: {n_videos} files")
|
||||
print(f" total: {total / 1e6:.1f} MB")
|
||||
print(f"local check: python -m http.server -d {out} 8080")
|
||||
print("deploy: upload the directory to any static host (Pages / nginx / ...)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user