From 1d0428b4a30e7861ae1f61e315da3c685d91524d Mon Sep 17 00:00:00 2001 From: voson Date: Mon, 27 Jul 2026 13:09:14 +0800 Subject: [PATCH] Add hash routing and Dota 2 logo to relations preview. - New web/relations/router.js (parseHash/serializeHash/installRouter): top tabs, hero + detail sub-tab, item, patch version, tag filters and search query all sync to URL (#/heroes/axe/core, ?tags=...&q=...); pushState for discrete picks, replaceState for debounced search. - export_relations_site.py copies router.js so static export deep-links. - Header shows dota2_logo.png + title, served via /ui-icon/ whitelist. - fetch_dota2_logo.py fetches the transparent emblem asset. - Docs: CHANGELOG / README / DESIGN / AGENTS updated. --- AGENTS.md | 4 +- CHANGELOG.md | 15 + DESIGN.md | 8 + README.md | 2 + assets/ui_icons/dota2_logo.png | Bin 0 -> 31569 bytes docs/climperor-web.pen | 9908 +++++++++++++++++++++++++++++++- export_relations_site.py | 2 +- fetch_dota2_logo.py | 77 + serve_relations.py | 2 +- web/relations/app.js | 93 +- web/relations/index.html | 7 +- web/relations/router.js | 151 + web/relations/style.css | 54 +- 13 files changed, 10306 insertions(+), 17 deletions(-) create mode 100644 assets/ui_icons/dota2_logo.png create mode 100644 fetch_dota2_logo.py create mode 100644 web/relations/router.js diff --git a/AGENTS.md b/AGENTS.md index bfd327a..1bff11f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,8 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段 | `data/hero_fear_overrides.json` | 英雄→害怕装备手工加减(合并进 item_fears) | | `data/hero_abilities.json` | 英雄技能与机制汇总(由 `fetch_hero_abilities.py` 生成) | | `data/hero_item_fears.json` | 英雄怕的装备(规则推导;预览「怕」行) | -| `web/relations/` | 关系只读预览页静态资源 | +| `web/relations/` | 关系只读预览页静态资源(`index.html` / `app.js` / `style.css` / `router.js`) | +| `web/relations/router.js` | Hash 路由:`parseHash` / `serializeHash` / `installRouter` / `syncStateToUrl`;状态↔URL 双向同步(顶层标签 / 英雄 + 子标签 / 物品 / 版本 / 标签筛选 / 搜索) | | `templates/cdn/` | 全英雄 CDN 模板(需提交或由脚本生成) | | `templates/roles/` / `templates/modes/` | 位置 / 模式辅助模板 | | `assets/role_icons/` | Valve 选人筛选角色图标(`filter_*`,品红 chroma 去黑边) | @@ -131,6 +132,7 @@ python evaluate.py - 代码标识符、日志、错误信息、注释用**英文**;用户可见摘要可用中文。 - 改匹配阈值或裁切时,用 `evaluate.py` / 标注帧验证,并更新 `CHANGELOG.md` 与必要时的 `DESIGN.md`。 - 改 GSI cfg 时同步核对 `gsi_setup.py` 与 Dota `gamestate_integration` 目录。 +- 改预览路由形态(URL 段 / query 参数 / 默认值)时同步 `web/relations/router.js`(`parseHash` / `serializeHash`)与 `app.js` 的 `applyPatch` 校验;新增可路由状态维度时在两处都加,并在 `syncStateToUrl` 调用点(含搜索 debounce)接好。Hash 路由不命中后端,`serve_relations.py` 无需改;`export_relations_site.py` 的导出文件列表须含 `router.js`。 - 改 `data/heroes.json` 结构时同步 `grid.py`(依赖 `attr` / `name_loc`);`roles` 供 `overlay.py` 使用;`aliases` 为中文口语/俗称(勿与 `name_loc` 重复),重跑 `fetch_cdn_templates.py` 会按 `key` 合并保留;`tags` 为中文定位(核心/辅助/…/幻象,由 `roles`+幻想系推导),仅关系预览筛选用;基础属性/血蓝等数值字段供关系预览详情条,勿塞机制文案。 - 不要重新引入 real 模板双层库、`cdn_penalty`、`build_library.py`。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 06dffc8..9a3665f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,21 @@ 不再被牵连出滚轮;压缩参数行距使典型技能在高屏零滚轮(无头 Chrome + CDP 实测 `scrollHeight == clientHeight` 验证)。整块宽度上限 1000→1320px、视频 700→820px、 详情列 720→760px。 +- 关系预览顶栏左上角新增 Dota 2 logo +「上分帝」标题(`assets/ui_icons/dota2_logo.png` + 透明 emblem),经 `/ui-icon/` 路由与静态导出统一分发;窄屏自动隐藏标题或整组标识, + 不影响居中分页布局。 +- 关系预览新增 **Hash 路由**:顶层标签(英雄 / 物品 / 版本)、选中英雄 + 详情子标签 + (技能 / 核心装备 / 被克装备 / 版本变更)、物品页选中物品、版本页选中版本均同步进 + URL;英雄页的定位标签筛选与搜索框文本以 query 参数(`?tags=核心,辅助&q=axe`)进 + URL。形态如 `#/heroes/axe/core`、`#/items/black_king_bar`、`#/patches/7.41` + (`#/patches` = 最新版,序列化时省略与最新同名的版本号)。刷新保状态、可分享 / + 深度链接、浏览器前进后退还原;搜索框 300ms debounce + `replaceState`(不刷历史栈), + 标签筛选用 `replaceState`,其余离散选择用 `pushState`。坏链接(未知英雄 / 物品 / + 版本)优雅降级为丢弃该项。新增 `web/relations/router.js`(`parseHash` / + `serializeHash` / `installRouter`),`app.js` 在 `main()` 装载并首屏 `applyUrlToState`; + `export_relations_site.py` 静态导出列表补入 `router.js`,静态站点深度链接同样可用 + (可部署 GitHub Pages,无需 rewrite)。Hash 路由不命中后端路径,`serve_relations.py` + 零改动。 ### Fixed diff --git a/DESIGN.md b/DESIGN.md index 5a594dc..3c8880e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -229,6 +229,14 @@ Dota 2 (-gamestateintegration) - **展示**:网格左上角青色方标 + 名次 1/2/3(区别于 Plus 右上三角)。 - **预览**:`serve_relations.py` 仿选将网格只读查看;数据手改 `data/relations.json` 或从 xlsx 导入。 另有顶级 **物品** 页(基础/升级,网格↔竖式布局),目录见 `data/item_shop.json`。 +- **预览 Hash 路由**:状态(顶层标签 / 选中英雄 + 详情子标签 / 选中物品 / 选中版本 / + 标签筛选 / 搜索)双向同步进 URL(`#/heroes/axe/core?tags=核心&q=axe` 等)。选 Hash + 而非 Path 路由:静态导出(GitHub Pages)无需 rewrite、`serve_relations.py` 零改动、 + 相对路径子站亦可用。写 URL 用 `history.pushState`/`replaceState`(静默,不触发 + `hashchange`,无回环);前进/后退靠 `hashchange` 监听重 parse + 校验 + render。 + 搜索 debounce 300ms + `replaceState` 防刷历史栈;坏链接丢弃该项不崩。路由逻辑集中在 + `web/relations/router.js`,`app.js` 只在 `main()` 装 `installRouter` + 各 state 变更点 + 调 `syncStateToUrl`。 - **不做**:避用标、Steam 登录页、爬 Dotabuff;关系**不**写入 `data/heroes.json`。 --- diff --git a/README.md b/README.md index 21a6481..fd78d08 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ python fetch_patches.py # 近一年版本日志 + 详情 → data/patches python serve_relations.py # 只读预览 http://127.0.0.1:8765(改 JSON 后刷新) ``` +预览页带 **Hash 路由**:`#/heroes/axe/core`、`#/items/black_king_bar`、`#/patches/7.41`(`#/patches` = 最新);英雄页的定位标签筛选与搜索框也进 URL(`?tags=核心&q=axe`)。刷新保状态、可分享 / 深度链接、浏览器前进后退还原。`python export_relations_site.py` 导出的静态站点同样支持深度链接(可部署 GitHub Pages)。 + 仿局内选将四列网格(图标来自 [dota2.com/heroes](https://www.dota2.com/heroes) 竖版裁切):点英雄高亮克制 / 被克制 / 搭档,下方显示定位,并以标签页切换 **技能**(可点选魔晶/神杖升级与天赋树)、**核心装备**、**被克装备**、**版本变更**(该英雄近一年各版本改动)。 英雄页上方网格固定高度;点选技能 / 核心装备 / 被克装备后,详情显示在下方(装备含合成,非弹窗)。 diff --git a/assets/ui_icons/dota2_logo.png b/assets/ui_icons/dota2_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..01a06f859df36937f795fff553fe5901aac0876e GIT binary patch literal 31569 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 None: out.mkdir(parents=True) # Frontend (uses relative paths — works from any sub-path). - for name in ("index.html", "app.js", "style.css"): + for name in ("index.html", "router.js", "app.js", "style.css"): shutil.copy2(WEB_DIR / name, out / name) # Data snapshot (same payload as serve_relations /api/data). diff --git a/fetch_dota2_logo.py b/fetch_dota2_logo.py new file mode 100644 index 0000000..0e34af6 --- /dev/null +++ b/fetch_dota2_logo.py @@ -0,0 +1,77 @@ +"""Fetch the official Dota 2 emblem icon (no wordmark) for the preview header. + +Source (discovered, not guessed) from the dota2.com.cn site HTML: + + +That .ico is a multi-frame container whose largest frames are 256x256 PNGs of +the red Dota 2 map-marker emblem with a transparent background — i.e. the +brand icon alone, no "DOTA 2" text. We extract the first 256x256 PNG frame, +decode with OpenCV, and write it to assets/ui_icons/dota2_logo.png so it rides +the existing /ui-icon/ route + static-export glob pipeline. +""" +import struct +import sys + +import cv2 +import numpy as np + +from common import UI_ICONS +from http_utils import http_bytes + +FAVICON_URL = "https://www.dota2.com.cn/favicon.ico" +OUT = UI_ICONS / "dota2_logo.png" +TARGET_SIZE = 256 + + +def _extract_largest_png_frame(data: bytes) -> bytes | None: + """Return the bytes of the largest PNG-embedded frame in an ICO blob.""" + if len(data) < 6 or data[:4] != b"\x00\x00\x01\x00": + return None + count = struct.unpack(" len(data): + break + w = data[off] + size = struct.unpack(" best[0]: + best = (width * width, blob) + return best[1] if best else None + + +def main() -> int: + if OUT.is_file() and OUT.stat().st_size >= 512: + print(f"exists: {OUT} ({OUT.stat().st_size} bytes)") + return 0 + data = http_bytes(FAVICON_URL, timeout=30) + if not data or len(data) < 16: + print("empty favicon download", file=sys.stderr) + return 1 + png = _extract_largest_png_frame(data) + if png is None: + print("no PNG frame in .ico", file=sys.stderr) + return 1 + arr = cv2.imdecode(np.frombuffer(png, dtype=np.uint8), cv2.IMREAD_UNCHANGED) + if arr is None: + print("decode failed", file=sys.stderr) + return 1 + UI_ICONS.mkdir(parents=True, exist_ok=True) + ok = cv2.imwrite(str(OUT), arr) + if not ok: + print("write failed", file=sys.stderr) + return 1 + h, w = arr.shape[:2] + channels = arr.shape[2] if arr.ndim == 3 else 1 + transparent = int(np.count_nonzero(arr[:, :, 3] == 0)) if channels == 4 else 0 + print(f"saved {OUT} ({w}x{h}, {OUT.stat().st_size} bytes, {transparent} px transparent)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/serve_relations.py b/serve_relations.py index 76b577f..cb9e454 100644 --- a/serve_relations.py +++ b/serve_relations.py @@ -448,7 +448,7 @@ class Handler(BaseHTTPRequestHandler): return if path.startswith("/ui-icon/"): key = path[len("/ui-icon/") :] - if key not in ("cooldown.png",): + if key not in ("cooldown.png", "dota2_logo.png"): self._json(400, {"error": "bad ui icon"}) return fpath = UI_ICONS / key diff --git a/web/relations/app.js b/web/relations/app.js index 785c206..7fc8254 100644 --- a/web/relations/app.js +++ b/web/relations/app.js @@ -147,6 +147,7 @@ function renderBoard() { function onHeroClick(key) { state.selectedKey = state.selectedKey === key ? null : key; state.inspect = null; + syncStateToUrl(); render(); } @@ -161,6 +162,7 @@ function renderTagbar() { allBtn.className = state.tagFilters.size ? "" : "active"; allBtn.addEventListener("click", () => { state.tagFilters.clear(); + syncStateToUrl({ replace: true }); render(); }); bar.appendChild(allBtn); @@ -172,6 +174,7 @@ function renderTagbar() { btn.addEventListener("click", () => { if (state.tagFilters.has(tag)) state.tagFilters.delete(tag); else state.tagFilters.add(tag); + syncStateToUrl({ replace: true }); render(); }); bar.appendChild(btn); @@ -1286,6 +1289,7 @@ function renderDetail() { // Reset inspect to the tab default (first skill / first item). state.inspect = null; ensureHeroInspectDefault(state.selectedKey); + syncStateToUrl(); renderDetail(); }); tabbar.appendChild(btn); @@ -1368,6 +1372,7 @@ function matchesItemQuery(key, meta) { function onShopItemClick(key) { state.selectedItemKey = state.selectedItemKey === key ? null : key; state.selectedKey = null; + syncStateToUrl(); render(); } @@ -1522,6 +1527,7 @@ function renderItemDetail() { mountItemInspect(root, meta, { onPickItem: (key) => { state.selectedItemKey = key; + syncStateToUrl(); render(); }, }); @@ -1718,6 +1724,7 @@ function renderPatches() { .join(""); select.onchange = () => { state.selectedPatch = select.value; + syncStateToUrl(); renderPatches(); const board = $("#patches-view"); if (board) board.scrollTo(0, 0); @@ -1780,6 +1787,7 @@ function setPage(page) { state.selectedKey = null; state.inspect = null; } + syncStateToUrl(); render(); } @@ -1815,6 +1823,71 @@ function render() { } } +// Search-box URL sync timer (debounced replace so typing does not spam history). +let searchSyncTimer = 0; +const SEARCH_SYNC_DEBOUNCE_MS = 300; + +/** + * Apply a router-produced state patch (from a parsed URL) to state, then render. + * Validates every field against loaded data so bad deep-links degrade gracefully + * (unknown hero/item/version is dropped rather than crashing the UI). + */ +function applyPatch(patch) { + if (!state.data) { + render(); + return; + } + // Page (default heroes on bad/missing). + if (patch.page && ["heroes", "items", "patches"].includes(patch.page)) { + state.page = patch.page; + } else { + state.page = "heroes"; + } + // Hero + detail sub-tab. + if (patch.heroKey && heroByKey(patch.heroKey)) { + state.selectedKey = patch.heroKey; + if ( + patch.detailTab && + ["skills", "core", "fears", "patches"].includes(patch.detailTab) + ) { + state.detailTab = patch.detailTab; + } else { + state.detailTab = "skills"; + } + } else { + state.selectedKey = null; + state.detailTab = "skills"; + } + // Reset inspect so ensureHeroInspectDefault re-picks per tab (inspect is URL-less). + state.inspect = null; + // Item (items page) — must exist in the shop catalog. + if (patch.itemKey && shopItem(patch.itemKey)) { + state.selectedItemKey = patch.itemKey; + } else { + state.selectedItemKey = null; + } + // Patch version (patches page; null means latest). + if ( + patch.patchVersion && + (state.data.patches || []).some((p) => p.version === patch.patchVersion) + ) { + state.selectedPatch = patch.patchVersion; + } else { + state.selectedPatch = null; + } + // Tag filters (heroes page only) — drop unknown tag names. + if (patch.tags instanceof Set) { + const valid = new Set(state.data.tag_order || []); + state.tagFilters = new Set([...patch.tags].filter((t) => valid.has(t))); + } + if (typeof patch.query === "string") { + state.query = patch.query; + const inp = $("#q"); + if (inp && inp.value !== state.query) inp.value = state.query; + } + render(); +} + function bindSearch() { const input = $("#q"); if (input) { @@ -1822,6 +1895,12 @@ function bindSearch() { input.addEventListener("input", () => { state.query = input.value; if (state.page === "heroes") renderBoard(); + // Debounced replace so each keystroke does not push a history entry. + if (searchSyncTimer) clearTimeout(searchSyncTimer); + searchSyncTimer = setTimeout(() => { + searchSyncTimer = 0; + syncStateToUrl({ replace: true }); + }, SEARCH_SYNC_DEBOUNCE_MS); }); } const itemInput = $("#q-item"); @@ -1830,6 +1909,11 @@ function bindSearch() { itemInput.addEventListener("input", () => { state.itemQuery = itemInput.value; if (state.page === "items") renderItemShop(); + if (searchSyncTimer) clearTimeout(searchSyncTimer); + searchSyncTimer = setTimeout(() => { + searchSyncTimer = 0; + syncStateToUrl({ replace: true }); + }, SEARCH_SYNC_DEBOUNCE_MS); }); } } @@ -1851,12 +1935,14 @@ async function main() { if (state.query && document.activeElement === $("#q")) { state.query = ""; $("#q").value = ""; + syncStateToUrl({ replace: true }); renderBoard(); return; } if (state.selectedKey) { state.selectedKey = null; state.inspect = null; + syncStateToUrl(); render(); } return; @@ -1864,11 +1950,13 @@ async function main() { if (state.itemQuery && document.activeElement === $("#q-item")) { state.itemQuery = ""; $("#q-item").value = ""; + syncStateToUrl({ replace: true }); renderItemShop(); return; } if (state.selectedItemKey) { state.selectedItemKey = null; + syncStateToUrl(); render(); } }); @@ -1909,7 +1997,10 @@ async function main() { state.data.patches = state.data.patches || []; state.data.patch_lookup = state.data.patch_lookup || { items: {}, abilities: {}, heroes: {} }; state.data.patch_details = state.data.patch_details || {}; - render(); + // Install hash router now that state.data is loaded (validation needs it), + // then apply the initial URL (deep-link support) which also renders. + installRouter({ getState: () => state, applyPatch }); + applyUrlToState(); } main().catch((err) => { diff --git a/web/relations/index.html b/web/relations/index.html index 5b1cfe0..e24d815 100644 --- a/web/relations/index.html +++ b/web/relations/index.html @@ -8,6 +8,10 @@
+
+ + 上分帝 +