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:
voson
2026-07-27 11:56:51 +08:00
parent e567a5cdfc
commit a91789b72f
76 changed files with 109860 additions and 996 deletions
+154
View File
@@ -0,0 +1,154 @@
"""Import qualitative relations from a Chinese draft spreadsheet.
Usage:
python import_relations_xlsx.py
python import_relations_xlsx.py "C:\\Users\\Administrator\\Downloads\\dota2.xlsx"
Writes data/relations.json. Skips category phrases (辅助/幻想系/...).
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from common import ROOT
from relations import (
DEFAULT_RELATIONS,
build_name_index,
load_relations,
resolve_name,
save_relations,
set_counter,
set_synergy,
split_names,
)
# Phrases that are roles/tags, not single heroes.
SKIP_TOKENS = {
"辅助",
"推进",
"幻想系",
"幻想",
"所有肉核",
"肉核",
"高爆发",
"奶妈",
"核心",
}
def _should_skip(token: str) -> bool:
t = token.strip()
if not t:
return True
if t in SKIP_TOKENS:
return True
if "" in t and t.endswith(""):
return True
if t.startswith("所有"):
return True
if "魔晶" in t or "a仗" in t.lower():
# item/facet interactions in hero columns — skip for edge seed
return True
return False
def import_xlsx(path: Path) -> tuple[dict, list[str]]:
import openpyxl
wb = openpyxl.load_workbook(path, data_only=True)
index = build_name_index()
data = load_relations()
data["meta"] = {
"source": str(path),
"imported_at": datetime.now(timezone.utc).isoformat(),
"note": "qualitative counters/synergies; no winrate",
}
unresolved: list[str] = []
sheets = ["力量", "敏捷", "智力", "全才"]
def resolve_list(cell, context: str) -> list[str]:
keys = []
for tok in split_names(cell):
if _should_skip(tok):
unresolved.append(f"{context}: skip tag '{tok}'")
continue
key = resolve_name(tok, index)
if key is None:
unresolved.append(f"{context}: unresolved '{tok}'")
continue
keys.append(key)
return keys
for sheet in sheets:
if sheet not in wb.sheetnames:
continue
ws = wb[sheet]
rows = list(ws.iter_rows(values_only=True))
# find header
header_i = None
for i, row in enumerate(rows):
if row and row[0] == "英雄":
header_i = i
break
if header_i is None:
continue
for row in rows[header_i + 1 :]:
if not row or not row[0]:
continue
hero_raw = str(row[0]).strip()
hero = resolve_name(hero_raw, index)
if hero is None:
unresolved.append(f"{sheet}: hero unresolved '{hero_raw}'")
continue
# col1 被克制, col2 克制, col3 搭档
countered_by = resolve_list(row[1] if len(row) > 1 else None, f"{hero_raw}/被克")
counters = resolve_list(row[2] if len(row) > 2 else None, f"{hero_raw}/克制")
partners = resolve_list(row[3] if len(row) > 3 else None, f"{hero_raw}/搭档")
for other in counters:
set_counter(data, hero, other, reason="")
for other in countered_by:
# other counters hero
set_counter(data, other, hero, reason="")
for other in partners:
set_synergy(data, hero, other, reason="")
return data, unresolved
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument(
"xlsx",
nargs="?",
default=str(Path.home() / "Downloads" / "dota2.xlsx"),
help="path to dota2.xlsx",
)
ap.add_argument("--out", default=str(DEFAULT_RELATIONS))
args = ap.parse_args()
path = Path(args.xlsx)
if not path.is_file():
raise SystemExit(f"file not found: {path}")
data, unresolved = import_xlsx(path)
out = save_relations(data, args.out)
print(f"wrote {out}")
print(f"counters={len(data['counters'])} synergies={len(data['synergies'])}")
report = ROOT / "data" / "relations_import_report.json"
report.write_text(
json.dumps({"unresolved": unresolved, "count": len(unresolved)}, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"unresolved notes: {len(unresolved)}{report}")
for line in unresolved[:40]:
print(" ", line)
if len(unresolved) > 40:
print(f" ... +{len(unresolved) - 40} more")
if __name__ == "__main__":
main()