v0.6.5: size hero detail drawer from viewport; add PC post-lock item tips.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 17:08:32 +08:00
co-authored by Cursor
parent fefd8c7823
commit 22a9cf256d
16 changed files with 603 additions and 128 deletions
+88 -15
View File
@@ -43,6 +43,7 @@ from capture import grab_frame, is_dota_foreground, raw_dir_for_match, save_fram
from shared.grid import bans, hero_table, read_grid
from modes import detect_mode, load_mode_templates
from recognize import recognize_image
from item_suggest import suggest_items
from recommend import ally_keys, enemy_keys, load_relations, suggest_marks
from roles import ROLES, detect_roles, load_role_templates
@@ -95,10 +96,18 @@ class DraftSession:
self.recommend_archetypes = bool(rec.get("archetypes", True))
self.recommend_role_tags = rec.get("role_tags")
self.relations = load_relations(rec.get("relations_path")) if self.recommend_enabled else None
items_cfg = rec.get("items") or {}
self.recommend_items_enabled = bool(items_cfg.get("enabled", True))
self.recommend_items_core_n = int(items_cfg.get("core_n", 3))
self.recommend_items_answer_n = int(items_cfg.get("answer_n", 3))
self.recommend_items_max_total = int(items_cfg.get("max_total", 6))
self.recommend_hero_items_path = items_cfg.get("hero_items_path")
self.recommend_items_meta_path = items_cfg.get("items_meta_path")
self._rec_warned = False
self._last_rec_sig: tuple | None = None
self._last_enemy_profile: dict = {}
self._last_rec_meta: dict = {}
self._last_item_sig: tuple | None = None
def _push_overlay(self, confirmed: dict[int, str]) -> None:
if self.overlay is None:
@@ -113,6 +122,7 @@ class DraftSession:
cells: dict | None,
picks: list[dict],
analysis: str = "",
items: list[dict] | None = None,
) -> None:
if self.overlay is None:
return
@@ -121,6 +131,8 @@ class DraftSession:
self.overlay.set_grid_marks(cells or {}, marks)
if hasattr(self.overlay, "set_analysis"):
self.overlay.set_analysis(analysis or "")
if hasattr(self.overlay, "set_items"):
self.overlay.set_items(items if items is not None else [])
except Exception as e: # noqa: BLE001
self.log(f"[draft] rec overlay update failed: {e}")
@@ -134,9 +146,12 @@ class DraftSession:
self.overlay.set_grid_marks({}, {})
if hasattr(self.overlay, "set_analysis"):
self.overlay.set_analysis("")
if hasattr(self.overlay, "set_items"):
self.overlay.set_items([])
self.overlay.show()
except Exception as e: # noqa: BLE001
self.log(f"[draft] overlay show failed: {e}")
self._last_item_sig = None
if self.recommend_enabled and not self._rec_warned:
rel = self.relations or {}
if not rel.get("counters") and not rel.get("synergies"):
@@ -157,6 +172,7 @@ class DraftSession:
"cells": None,
"mode": None,
"recommendations": [],
"item_recommendations": [],
}
polls = 0
last_frame = None
@@ -248,8 +264,8 @@ class DraftSession:
event["frame"] = save_frame(frame, self.frame_dir, prefix="draft")
timeline.append(event)
self._log_event(event, info)
if state == HERO_SELECTION:
self._refresh_recommendations(confirmed, info, gsi_fn)
# Hero marks in selection; item bar after self-lock (incl. strategy tail).
self._refresh_recommendations(confirmed, info, gsi_fn)
n = len(confirmed)
if self.keep_frames and n in (4, 8, 10) and n not in saved_milestones:
@@ -276,6 +292,8 @@ class DraftSession:
self.overlay.set_grid_marks({}, {})
if hasattr(self.overlay, "set_analysis"):
self.overlay.set_analysis("")
if hasattr(self.overlay, "set_items"):
self.overlay.set_items([])
self.overlay.hide()
except Exception as e: # noqa: BLE001
self.log(f"[draft] overlay hide failed: {e}")
@@ -383,26 +401,70 @@ class DraftSession:
self.log(f"[draft] grid: {len(res['unavailable'])} heroes unavailable "
f"(contrast margin {res['margin']}) - {names}")
def _refresh_item_recommendations(
self,
confirmed: dict,
info: dict,
*,
self_hero: str,
enemies: list[str],
force: bool = False,
) -> None:
"""After self-lock: clear hero marks and show core + answer items."""
analysis = (self._last_rec_meta or {}).get("analysis") or ""
if not self.recommend_items_enabled:
if info.get("recommendations") or info.get("item_recommendations") or self._last_rec_sig != ("locked",):
info["recommendations"] = []
info["item_recommendations"] = []
self._last_rec_sig = ("locked",)
self._last_item_sig = ("locked-off",)
self._push_rec_overlay(info.get("cells"), [], analysis, [])
return
sig = (self_hero, tuple(enemies))
if not force and sig == self._last_item_sig:
return
self._last_item_sig = sig
self._last_rec_sig = ("locked",)
info["recommendations"] = []
items = suggest_items(
self_hero,
enemies,
core_n=self.recommend_items_core_n,
answer_n=self.recommend_items_answer_n,
max_total=self.recommend_items_max_total,
hero_items_path=self.recommend_hero_items_path,
items_meta_path=self.recommend_items_meta_path,
)
info["item_recommendations"] = items
self._push_rec_overlay(info.get("cells"), [], analysis, items)
if items:
bits = [
f"{it.get('name_loc') or it['key']}({it.get('reason') or it.get('kind')})"
for it in items
]
self.log(
f"[rec:items] {self.hero_names.get(self_hero, self_hero)} vs "
f"{[self.hero_names.get(e, e) for e in enemies]}: {', '.join(bits)}"
)
def _refresh_recommendations(self, confirmed: dict, info: dict, gsi_fn, *, force: bool = False) -> None:
if not self.recommend_enabled:
return
if not self.relations and not self.recommend_archetypes:
return
gsi = (gsi_fn() or {}) if gsi_fn else {}
self_slot = gsi_slot(gsi) or info.get("self_slot")
self_team = gsi.get("team") or info.get("self_team")
role = info["roles"].get(self_slot) if self_slot else None
position = role["position"] if role else None
# Stop suggesting once you have locked a hero.
if self_slot and confirmed.get(self_slot):
if info.get("recommendations") or self._last_rec_sig is not None:
info["recommendations"] = []
self._last_enemy_profile = {}
self._last_rec_meta = {}
self._last_rec_sig = ("locked",)
self._push_rec_overlay(info.get("cells"), [], "")
return
self_hero = (confirmed.get(self_slot) if self_slot else None) or gsi.get("hero")
enemies = enemy_keys(confirmed, self_team)
# Once locked, switch from hero marks to item icon bar.
if self_slot and self_hero:
self._refresh_item_recommendations(
confirmed, info, self_hero=self_hero, enemies=enemies, force=force,
)
return
if not self.relations and not self.recommend_archetypes:
return
allies = ally_keys(confirmed, self_team, self_slot)
exclude = set(confirmed.values())
if info.get("unavailable"):
@@ -411,11 +473,13 @@ class DraftSession:
if not force and sig == self._last_rec_sig:
return
self._last_rec_sig = sig
self._last_item_sig = None
if len(enemies) < self.recommend_min_enemies:
info["recommendations"] = []
info["item_recommendations"] = []
self._last_enemy_profile = {}
self._last_rec_meta = {}
self._push_rec_overlay(info.get("cells"), [], "")
self._push_rec_overlay(info.get("cells"), [], "", [])
return
result = suggest_marks(
position=position,
@@ -433,6 +497,7 @@ class DraftSession:
profile = result.get("enemy_profile") or {}
analysis = result.get("analysis") or ""
info["recommendations"] = picks
info["item_recommendations"] = []
self._last_enemy_profile = profile
self._last_rec_meta = {
"analysis": analysis,
@@ -441,7 +506,7 @@ class DraftSession:
"ally_gaps": list(result.get("ally_gaps") or []),
"ally_profile": dict(result.get("ally_profile") or {}),
}
self._push_rec_overlay(info.get("cells"), picks, analysis)
self._push_rec_overlay(info.get("cells"), picks, analysis, [])
if picks or analysis:
bits = []
for p in picks[:12]:
@@ -597,6 +662,7 @@ class DraftSession:
"ally_gaps": list((self._last_rec_meta or {}).get("ally_gaps") or []),
"analysis": (self._last_rec_meta or {}).get("analysis") or "",
"picks": info.get("recommendations") or [],
"items": info.get("item_recommendations") or [],
},
}
if info["unavailable"] is not None:
@@ -680,6 +746,13 @@ def describe(summary: dict) -> list[str]:
name = p.get("name_loc") or loc(p["key"], names)
bits.append(f"{name}[{labs}]" + (f"({why})" if why else ""))
lines.append(f"rec : {len(picks)}{', '.join(bits)}")
items = rec.get("items") or []
if items:
bits = [
f"{it.get('name_loc') or it.get('key')}({it.get('reason') or it.get('kind')})"
for it in items
]
lines.append(f"items : {', '.join(bits)}")
return lines