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:
@@ -0,0 +1,298 @@
|
||||
"""Draft item suggestions: hero core builds + qualitative answers vs enemies.
|
||||
|
||||
Core items come from web/data/hero_items.json (relative popularity).
|
||||
Answer items are rule-mapped from enemy tags / push-global archetypes.
|
||||
Does not read hero_item_fears stats or STRATZ.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
from draft_archetypes import detect_archetypes, tag_profile
|
||||
from shared.grid import hero_table
|
||||
from shared.hero_tags import tags_for_hero
|
||||
from shared.paths import DATA, ROOT
|
||||
|
||||
DEFAULT_HERO_ITEMS = DATA / "hero_items.json"
|
||||
DEFAULT_ITEMS_META = DATA / "items_meta.json"
|
||||
|
||||
# Heroes that commonly pick from fog / invis — qualitative only.
|
||||
INVIS_HEROES = frozenset({
|
||||
"riki",
|
||||
"bounty_hunter",
|
||||
"clinkz",
|
||||
"weaver",
|
||||
"nyx_assassin",
|
||||
"templar_assassin",
|
||||
"mirana",
|
||||
"treant",
|
||||
"windrunner",
|
||||
"slark",
|
||||
"invoker",
|
||||
"sand_king",
|
||||
})
|
||||
|
||||
# Soft heal / sustain cores — vessel answers.
|
||||
HEAL_HEROES = frozenset({
|
||||
"omniknight",
|
||||
"winter_wyvern",
|
||||
"bane",
|
||||
"undying",
|
||||
"abaddon",
|
||||
"oracle",
|
||||
"chen",
|
||||
"io",
|
||||
"wisp",
|
||||
"dazzle",
|
||||
"warlock",
|
||||
})
|
||||
|
||||
# Fallback Chinese names when catalogs miss an entry.
|
||||
_FALLBACK_NAMES = {
|
||||
"black_king_bar": "黑皇杖",
|
||||
"bfury": "狂战斧",
|
||||
"disperser": "散魂剑",
|
||||
"gem": "真视宝石",
|
||||
"dust": "显影之尘",
|
||||
"pipe": "洞察烟斗",
|
||||
"eternal_shroud": "永世护盾",
|
||||
"travel_boots": "远行鞋",
|
||||
"crimson_guard": "赤红甲",
|
||||
"spirit_vessel": "魂之灵瓮",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_path(path: str | Path | None, default: Path) -> Path:
|
||||
if not path:
|
||||
return default
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = ROOT / p
|
||||
return p
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _load_hero_items(path_str: str) -> dict:
|
||||
path = Path(path_str)
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _load_items_meta(path_str: str) -> dict[str, str]:
|
||||
path = Path(path_str)
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
items = data.get("items") if isinstance(data, dict) else None
|
||||
if not isinstance(items, dict):
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
for row in items.values():
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
key = row.get("key")
|
||||
if not key:
|
||||
continue
|
||||
name = row.get("name_loc") or row.get("dname") or key
|
||||
out[str(key)] = str(name)
|
||||
return out
|
||||
|
||||
|
||||
def _tags_by_key() -> dict[str, list[str]]:
|
||||
return {
|
||||
h["key"]: list(h.get("tags") or []) or tags_for_hero(h["key"], h.get("roles"))
|
||||
for h in hero_table()
|
||||
}
|
||||
|
||||
|
||||
def _name_for(
|
||||
key: str,
|
||||
*,
|
||||
catalog: dict[str, dict],
|
||||
meta_names: dict[str, str],
|
||||
) -> str:
|
||||
row = catalog.get(key) or {}
|
||||
return (
|
||||
str(row.get("name_loc") or "")
|
||||
or meta_names.get(key)
|
||||
or _FALLBACK_NAMES.get(key)
|
||||
or key
|
||||
)
|
||||
|
||||
|
||||
def _catalog_from_hero_items(data: dict) -> dict[str, dict]:
|
||||
"""id-str -> row and also key -> row for lookups."""
|
||||
items = data.get("items") or {}
|
||||
by_key: dict[str, dict] = {}
|
||||
for row in items.values():
|
||||
if isinstance(row, dict) and row.get("key"):
|
||||
by_key[str(row["key"])] = row
|
||||
return by_key
|
||||
|
||||
|
||||
def core_items_for_hero(
|
||||
self_hero: str,
|
||||
data: dict,
|
||||
*,
|
||||
core_n: int = 3,
|
||||
catalog: dict[str, dict] | None = None,
|
||||
meta_names: dict[str, str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Top relative-popularity finished items for one hero."""
|
||||
if not self_hero or core_n <= 0:
|
||||
return []
|
||||
by_hero = data.get("by_hero") or {}
|
||||
rows = list(by_hero.get(self_hero) or [])
|
||||
items_by_id = data.get("items") or {}
|
||||
catalog = catalog if catalog is not None else _catalog_from_hero_items(data)
|
||||
meta_names = meta_names or {}
|
||||
out: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for row in rows:
|
||||
if len(out) >= core_n:
|
||||
break
|
||||
iid = str(row.get("id") if isinstance(row, dict) else row)
|
||||
meta = items_by_id.get(iid) or items_by_id.get(int(iid) if iid.isdigit() else iid) or {}
|
||||
key = str(meta.get("key") or "")
|
||||
if not key or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append({
|
||||
"key": key,
|
||||
"name_loc": _name_for(key, catalog=catalog, meta_names=meta_names),
|
||||
"kind": "core",
|
||||
"reason": "常用",
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def answer_items_for_enemies(
|
||||
enemies: list[str],
|
||||
*,
|
||||
answer_n: int = 3,
|
||||
catalog: dict[str, dict] | None = None,
|
||||
meta_names: dict[str, str] | None = None,
|
||||
tags_by_key: dict[str, list[str]] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Qualitative counter items from enemy tags / archetypes."""
|
||||
if answer_n <= 0 or not enemies:
|
||||
return []
|
||||
tags_by_key = tags_by_key or _tags_by_key()
|
||||
catalog = catalog or {}
|
||||
meta_names = meta_names or {}
|
||||
profile = tag_profile(enemies, tags_by_key)
|
||||
arches = detect_archetypes(enemies, tags_by_key)
|
||||
|
||||
candidates: list[tuple[str, str]] = []
|
||||
|
||||
control_n = int(profile.get("控制") or 0) + int(profile.get("先手") or 0)
|
||||
if control_n >= 2:
|
||||
candidates.append(("black_king_bar", "克控制"))
|
||||
|
||||
if int(profile.get("幻象") or 0) >= 1 or any(
|
||||
"幻象" in (tags_by_key.get(e) or []) for e in enemies
|
||||
):
|
||||
candidates.append(("bfury", "清幻象"))
|
||||
candidates.append(("disperser", "打幻象"))
|
||||
|
||||
if any(e in INVIS_HEROES for e in enemies):
|
||||
candidates.append(("gem", "克隐身"))
|
||||
candidates.append(("dust", "显影"))
|
||||
|
||||
if int(profile.get("爆发") or 0) >= 2:
|
||||
candidates.append(("pipe", "克魔法"))
|
||||
candidates.append(("eternal_shroud", "魔抗"))
|
||||
|
||||
if any(e in HEAL_HEROES for e in enemies):
|
||||
candidates.append(("spirit_vessel", "克回复"))
|
||||
|
||||
if "push" in arches:
|
||||
candidates.append(("crimson_guard", "抗推进"))
|
||||
candidates.append(("travel_boots", "对推进"))
|
||||
if "global" in arches:
|
||||
candidates.append(("travel_boots", "对全球流"))
|
||||
|
||||
out: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for key, reason in candidates:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append({
|
||||
"key": key,
|
||||
"name_loc": _name_for(key, catalog=catalog, meta_names=meta_names),
|
||||
"kind": "answer",
|
||||
"reason": reason,
|
||||
})
|
||||
if len(out) >= answer_n:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def suggest_items(
|
||||
self_hero: str | None,
|
||||
enemies: list[str] | None,
|
||||
*,
|
||||
core_n: int = 3,
|
||||
answer_n: int = 3,
|
||||
max_total: int = 6,
|
||||
hero_items_path: str | Path | None = None,
|
||||
items_meta_path: str | Path | None = None,
|
||||
) -> list[dict]:
|
||||
"""Merge answer items then core items; dedupe by key; soft-fail to []."""
|
||||
if not self_hero or max_total <= 0:
|
||||
return []
|
||||
hi_path = _resolve_path(hero_items_path, DEFAULT_HERO_ITEMS)
|
||||
meta_path = _resolve_path(items_meta_path, DEFAULT_ITEMS_META)
|
||||
data = _load_hero_items(str(hi_path))
|
||||
if not data:
|
||||
return []
|
||||
catalog = _catalog_from_hero_items(data)
|
||||
meta_names = _load_items_meta(str(meta_path))
|
||||
answers = answer_items_for_enemies(
|
||||
list(enemies or []),
|
||||
answer_n=answer_n,
|
||||
catalog=catalog,
|
||||
meta_names=meta_names,
|
||||
)
|
||||
cores = core_items_for_hero(
|
||||
self_hero,
|
||||
data,
|
||||
core_n=core_n,
|
||||
catalog=catalog,
|
||||
meta_names=meta_names,
|
||||
)
|
||||
out: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for row in answers + cores:
|
||||
key = row.get("key")
|
||||
if not key or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(row)
|
||||
if len(out) >= max_total:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_HERO_ITEMS",
|
||||
"answer_items_for_enemies",
|
||||
"core_items_for_hero",
|
||||
"suggest_items",
|
||||
]
|
||||
Reference in New Issue
Block a user