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
+8 -37
View File
@@ -1,11 +1,7 @@
"""Read the role-queue labels and find which top-bar slot is you.
"""Read role-queue lane labels under top-bar portraits.
Two things live in the strip of text under each top-bar portrait:
row 1 (name) - your own name renders white and bold, everyone else's is
tinted blue, which is enough to tell which slot is you.
row 2 (role) - only drawn for your own team, and only in role-queue
(定位匹配) matches: 优势路 / 中路 / 劣势路 / 辅助 / 纯辅助.
Row under each portrait (role-queue only, own team): 优势路 / 中路 / 劣势路 /
辅助 / 纯辅助. Your own slot index comes from GSI team_slot, not from name tint.
The role text is flat grey with zero saturation, so a threshold on
value+saturation isolates it cleanly. Matching is done on the binary mask
@@ -118,9 +114,11 @@ def load_role_templates() -> dict[str, np.ndarray]:
def detect_roles(img: np.ndarray, cfg: dict, templates: dict[str, np.ndarray] | None = None) -> dict:
"""Per-slot role plus which slot is you.
"""Per-slot role labels from the role-queue text under top-bar portraits.
Returns {"self_slot": int|None, "self_team": str|None, "roles": {slot: {...}}}.
Returns {"self_team": str|None, "roles": {slot: {...}}}.
Your own top-bar slot comes from GSI team_slot elsewhere - this helper
does not guess it from name brightness.
Slots without a role label (the enemy team, or any non-role-queue mode)
are simply absent from "roles".
"""
@@ -144,38 +142,11 @@ def detect_roles(img: np.ndarray, cfg: dict, templates: dict[str, np.ndarray] |
"score": round(score, 3),
}
self_slot = _detect_self(img, cfg)
self_team = None
if roles:
self_team = "radiant" if min(roles) <= 5 else "dire"
elif self_slot is not None:
self_team = "radiant" if self_slot <= 5 else "dire"
return {"self_slot": self_slot, "self_team": self_team, "roles": roles}
def _detect_self(img: np.ndarray, cfg: dict) -> int | None:
"""Your own name is drawn bright white, the other nine a dimmer blue-grey.
The gap is ~55 units of brightness, so compare slots against each other
instead of a fixed threshold - that survives HUD skins and any screen
where every row happens to be bright (a menu, a loading overlay), because
there the runner-up is just as bright and the match is rejected.
"""
r = cfg.get("roles", {})
min_value = r.get("self_min_value", 195.0)
min_gap = r.get("self_min_gap", 25.0)
found = []
for slot in cfg.get("slots", []):
tint = name_tint(img, slot, cfg)
if tint is not None:
found.append((tint[0], slot["index"]))
if len(found) < 2:
return None
found.sort(reverse=True)
if found[0][0] < min_value or found[0][0] - found[1][0] < min_gap:
return None
return found[0][1]
return {"self_team": self_team, "roles": roles}
def _main() -> None: