"""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", ]