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:
+116
-85
@@ -1,7 +1,7 @@
|
||||
"""Transparent click-through overlay: role tags + 克/搭/补 marks + analysis bar.
|
||||
"""Transparent click-through overlay: 克/搭/补 marks + analysis bar + item icons.
|
||||
|
||||
Runs a Tk root on a background thread. DraftSession calls set_roster(),
|
||||
set_grid_marks(), and set_analysis(); geometry uses relative coords.
|
||||
Runs a Tk root on a background thread. DraftSession calls set_grid_marks(),
|
||||
set_analysis(), and set_items(); geometry uses relative coords.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,26 +11,12 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import json
|
||||
import threading
|
||||
import tkinter as tk
|
||||
|
||||
import mss
|
||||
|
||||
from common import HEROES_JSON as HEROES_PATH, ROOT, slot_rect_px
|
||||
|
||||
ROLE_ICONS_DIR = ROOT / "assets" / "role_icons"
|
||||
|
||||
ROLE_ORDER = [
|
||||
"Carry",
|
||||
"Support",
|
||||
"Nuker",
|
||||
"Disabler",
|
||||
"Durable",
|
||||
"Escape",
|
||||
"Initiator",
|
||||
"Pusher",
|
||||
]
|
||||
from shared.paths import ITEM_ICONS
|
||||
|
||||
CHROMA = "#ff00ff"
|
||||
DEFAULT_COUNTER_COLOR = "#2ec4b6"
|
||||
@@ -39,6 +25,9 @@ DEFAULT_FILL_COLOR = "#9b7ebd"
|
||||
DEFAULT_MARK_TEXT = "#0b1220"
|
||||
DEFAULT_ANALYSIS_BG = "#1a2332"
|
||||
DEFAULT_ANALYSIS_FG = "#e8eef7"
|
||||
DEFAULT_ITEM_CORE_BG = "#1a2332"
|
||||
DEFAULT_ITEM_ANSWER_BG = "#2a1f14"
|
||||
DEFAULT_ITEM_REASON_FG = "#e8eef7"
|
||||
LABEL_ORDER = ("克", "搭", "补")
|
||||
LABEL_COLORS = {
|
||||
"克": "counter",
|
||||
@@ -47,13 +36,6 @@ LABEL_COLORS = {
|
||||
}
|
||||
|
||||
|
||||
def _load_roles_by_key() -> dict[str, list[str]]:
|
||||
if not HEROES_PATH.exists():
|
||||
return {}
|
||||
table = json.loads(HEROES_PATH.read_text(encoding="utf-8"))
|
||||
return {h["key"]: list(h.get("roles") or []) for h in table}
|
||||
|
||||
|
||||
def _primary_monitor_size() -> tuple[int, int]:
|
||||
with mss.MSS() as sct:
|
||||
mon = sct.monitors[1]
|
||||
@@ -83,9 +65,6 @@ class DraftOverlay:
|
||||
def __init__(self, cfg: dict):
|
||||
self.cfg = cfg
|
||||
o = cfg.get("overlay") or {}
|
||||
self.y_gap_rel = float(o.get("y_gap_rel", 0.008))
|
||||
self.icon_h_rel = float(o.get("icon_h_rel", 0.016))
|
||||
self.icon_gap_rel = float(o.get("icon_gap_rel", 0.002))
|
||||
self.mark_size_rel = float(o.get("mark_size_rel", 0.018))
|
||||
self.mark_pad_rel = float(o.get("mark_pad_rel", 0.004))
|
||||
self.mark_gap_rel = float(o.get("mark_gap_rel", 0.002))
|
||||
@@ -98,11 +77,17 @@ class DraftOverlay:
|
||||
self.analysis_font_rel = float(o.get("analysis_font_rel", 0.014))
|
||||
self.analysis_bg = str(o.get("analysis_bg", DEFAULT_ANALYSIS_BG))
|
||||
self.analysis_fg = str(o.get("analysis_fg", DEFAULT_ANALYSIS_FG))
|
||||
self.roles_by_key = _load_roles_by_key()
|
||||
self._roster: dict[int, str] = {}
|
||||
self.items_y_rel = float(o.get("items_y_rel", 0.16))
|
||||
self.item_icon_h_rel = float(o.get("item_icon_h_rel", 0.036))
|
||||
self.item_gap_rel = float(o.get("item_gap_rel", 0.01))
|
||||
self.item_core_bg = str(o.get("item_core_bg", DEFAULT_ITEM_CORE_BG))
|
||||
self.item_answer_bg = str(o.get("item_answer_bg", DEFAULT_ITEM_ANSWER_BG))
|
||||
self.item_reason_fg = str(o.get("item_reason_fg", DEFAULT_ITEM_REASON_FG))
|
||||
self.item_icons_dir = Path(o.get("item_icons_dir") or ITEM_ICONS)
|
||||
self._cells: dict[str, dict] = {}
|
||||
self._marks: dict[str, list[str]] = {}
|
||||
self._analysis = ""
|
||||
self._items: list[dict] = []
|
||||
self._ready = threading.Event()
|
||||
self._closed = False
|
||||
self._root: tk.Tk | None = None
|
||||
@@ -137,7 +122,6 @@ class DraftOverlay:
|
||||
canvas.pack(fill="both", expand=True)
|
||||
self._canvas = canvas
|
||||
self._screen = (sw, sh)
|
||||
self._load_icon_sources()
|
||||
root.update_idletasks()
|
||||
try:
|
||||
hwnd = int(root.wm_frame(), 16) if root.wm_frame().startswith("0x") else int(root.winfo_id())
|
||||
@@ -157,38 +141,9 @@ class DraftOverlay:
|
||||
pass
|
||||
self._closed = True
|
||||
|
||||
def _load_icon_sources(self) -> None:
|
||||
assert self._root is not None
|
||||
for name in ROLE_ORDER:
|
||||
path = ROLE_ICONS_DIR / f"{name}.png"
|
||||
if not path.exists():
|
||||
continue
|
||||
try:
|
||||
self._icon_src[name] = tk.PhotoImage(master=self._root, file=str(path))
|
||||
except tk.TclError:
|
||||
continue
|
||||
|
||||
def _scaled_icon(self, name: str, target_h: int) -> tk.PhotoImage | None:
|
||||
src = self._icon_src.get(name)
|
||||
if src is None or target_h <= 0:
|
||||
return None
|
||||
h = max(src.height(), 1)
|
||||
if target_h >= h:
|
||||
factor = max(1, round(target_h / h))
|
||||
img = src.zoom(factor, factor)
|
||||
else:
|
||||
factor = max(1, round(h / target_h))
|
||||
img = src.subsample(factor, factor)
|
||||
self._photos.append(img)
|
||||
return img
|
||||
|
||||
def set_roster(self, confirmed: dict[int, str]) -> None:
|
||||
"""Update tags for confirmed slots (hero_key by slot index)."""
|
||||
roster = {int(k): v for k, v in confirmed.items() if v}
|
||||
if roster == self._roster:
|
||||
return
|
||||
self._roster = dict(roster)
|
||||
self._schedule_redraw()
|
||||
"""No-op kept for DraftSession compatibility (role tags under avatars removed)."""
|
||||
return
|
||||
|
||||
def set_analysis(self, text: str | None) -> None:
|
||||
"""Short lineup analysis banner (empty clears)."""
|
||||
@@ -198,6 +153,24 @@ class DraftOverlay:
|
||||
self._analysis = value
|
||||
self._schedule_redraw()
|
||||
|
||||
def set_items(self, items: list[dict] | None) -> None:
|
||||
"""Item icon bar after self-lock: [{key, name_loc, kind, reason}, ...]."""
|
||||
parsed: list[dict] = []
|
||||
for row in items or []:
|
||||
key = row.get("key")
|
||||
if not key:
|
||||
continue
|
||||
parsed.append({
|
||||
"key": str(key),
|
||||
"name_loc": str(row.get("name_loc") or key),
|
||||
"kind": str(row.get("kind") or "core"),
|
||||
"reason": str(row.get("reason") or ""),
|
||||
})
|
||||
if parsed == self._items:
|
||||
return
|
||||
self._items = parsed
|
||||
self._schedule_redraw()
|
||||
|
||||
def set_grid_marks(
|
||||
self,
|
||||
cells: dict[str, dict] | None,
|
||||
@@ -265,6 +238,7 @@ class DraftOverlay:
|
||||
self._canvas.delete("all")
|
||||
self._photos.clear()
|
||||
self._analysis = ""
|
||||
self._items = []
|
||||
root.withdraw()
|
||||
|
||||
try:
|
||||
@@ -296,6 +270,85 @@ class DraftOverlay:
|
||||
self._thread.join(timeout=2.0)
|
||||
self._closed = True
|
||||
|
||||
def _load_item_src(self, key: str) -> tk.PhotoImage | None:
|
||||
assert self._root is not None
|
||||
if key in self._icon_src:
|
||||
return self._icon_src[key]
|
||||
path = self.item_icons_dir / f"{key}.png"
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
img = tk.PhotoImage(master=self._root, file=str(path))
|
||||
except tk.TclError:
|
||||
return None
|
||||
self._icon_src[key] = img
|
||||
return img
|
||||
|
||||
def _scaled_item_icon(self, key: str, target_h: int) -> tk.PhotoImage | None:
|
||||
src = self._load_item_src(key)
|
||||
if src is None or target_h <= 0:
|
||||
return None
|
||||
h = max(src.height(), 1)
|
||||
if target_h >= h:
|
||||
factor = max(1, round(target_h / h))
|
||||
img = src.zoom(factor, factor)
|
||||
else:
|
||||
factor = max(1, round(h / target_h))
|
||||
img = src.subsample(factor, factor)
|
||||
self._photos.append(img)
|
||||
return img
|
||||
|
||||
def _draw_items(self, canvas: tk.Canvas, sw: int, sh: int) -> None:
|
||||
if not self._items:
|
||||
return
|
||||
icon_h = max(20, int(round(self.item_icon_h_rel * sh)))
|
||||
gap = max(4, int(round(self.item_gap_rel * sw)))
|
||||
reason_font = max(9, int(round(0.011 * sh)))
|
||||
pad = max(4, int(round(0.003 * sh)))
|
||||
top = int(round(self.items_y_rel * sh))
|
||||
|
||||
cells: list[tuple[dict, tk.PhotoImage | None, int, int]] = []
|
||||
for row in self._items:
|
||||
img = self._scaled_item_icon(row["key"], icon_h)
|
||||
iw = img.width() if img is not None else icon_h
|
||||
ih = img.height() if img is not None else icon_h
|
||||
cells.append((row, img, iw, ih))
|
||||
|
||||
cell_w = max((iw for _, _, iw, _ in cells), default=icon_h) + 2 * pad
|
||||
# Extra width for short Chinese reason under icon.
|
||||
cell_w = max(cell_w, reason_font * 4 + 2 * pad)
|
||||
cell_h = max((ih for _, _, _, ih in cells), default=icon_h) + reason_font + 3 * pad
|
||||
total_w = len(cells) * cell_w + gap * max(0, len(cells) - 1)
|
||||
left = max(0, (sw - total_w) // 2)
|
||||
|
||||
for i, (row, img, iw, ih) in enumerate(cells):
|
||||
x0 = left + i * (cell_w + gap)
|
||||
y0 = top
|
||||
x1 = x0 + cell_w
|
||||
y1 = y0 + cell_h
|
||||
bg = self.item_answer_bg if row.get("kind") == "answer" else self.item_core_bg
|
||||
canvas.create_rectangle(x0, y0, x1, y1, fill=bg, outline=bg)
|
||||
cx = (x0 + x1) / 2
|
||||
if img is not None:
|
||||
canvas.create_image(cx, y0 + pad + ih / 2, image=img, anchor="center")
|
||||
else:
|
||||
name = row.get("name_loc") or row["key"]
|
||||
canvas.create_text(
|
||||
cx,
|
||||
y0 + pad + icon_h / 2,
|
||||
text=name[:4],
|
||||
fill=self.item_reason_fg,
|
||||
font=("Microsoft YaHei UI", max(8, reason_font - 1), "bold"),
|
||||
)
|
||||
reason = (row.get("reason") or "").strip() or ("应对" if row.get("kind") == "answer" else "常用")
|
||||
canvas.create_text(
|
||||
cx,
|
||||
y1 - pad - reason_font / 2,
|
||||
text=reason[:6],
|
||||
fill=self.item_reason_fg,
|
||||
font=("Microsoft YaHei UI", reason_font),
|
||||
)
|
||||
|
||||
def _redraw(self) -> None:
|
||||
canvas = self._canvas
|
||||
if canvas is None:
|
||||
@@ -303,9 +356,6 @@ class DraftOverlay:
|
||||
canvas.delete("all")
|
||||
self._photos.clear()
|
||||
sw, sh = self._screen
|
||||
icon_h = max(8, int(round(self.icon_h_rel * sh)))
|
||||
gap = max(0, int(round(self.icon_gap_rel * sh)))
|
||||
y_gap = int(round(self.y_gap_rel * sh))
|
||||
|
||||
if self._analysis:
|
||||
bar_h = max(18, int(round(self.analysis_h_rel * sh)))
|
||||
@@ -327,26 +377,7 @@ class DraftOverlay:
|
||||
font=("Microsoft YaHei UI", font_size, "bold"),
|
||||
)
|
||||
|
||||
for slot in self.cfg.get("slots") or []:
|
||||
idx = int(slot["index"])
|
||||
hero = self._roster.get(idx)
|
||||
if not hero:
|
||||
continue
|
||||
roles = [r for r in ROLE_ORDER if r in set(self.roles_by_key.get(hero, []))]
|
||||
if not roles:
|
||||
continue
|
||||
x, y, w, h = slot_rect_px(slot, self.cfg, sw, sh)
|
||||
icons = [img for r in roles if (img := self._scaled_icon(r, icon_h)) is not None]
|
||||
if not icons:
|
||||
continue
|
||||
total_w = sum(img.width() for img in icons) + gap * (len(icons) - 1)
|
||||
cx = x + w / 2
|
||||
left = int(round(cx - total_w / 2))
|
||||
top = y + h + y_gap
|
||||
cursor = left
|
||||
for img in icons:
|
||||
canvas.create_image(cursor, top, image=img, anchor="nw")
|
||||
cursor += img.width() + gap
|
||||
self._draw_items(canvas, sw, sh)
|
||||
|
||||
mark = max(12, int(round(self.mark_size_rel * sh)))
|
||||
pad = max(2, int(round(self.mark_pad_rel * sh)))
|
||||
|
||||
Reference in New Issue
Block a user