@clawhub-yangjian1412-ba712bf96c
这是一个先锋分段萌新纯AI自制的抛砖引玉的Dota2攻略小技能,希望能帮助每个萌新下分,也希望能有越来越多的人帮忙完善更新,希望Make CNDOTA Great Again。PS:作为毒瘤冰女玩家,我夹带了冰女私货攻略。触发词:dota、dota2、dota攻略、dota出装、出装建议、dota打法、dota技...
---
name: dota2-coach
version: 1.1.3
description: 这是一个先锋分段萌新纯AI自制的抛砖引玉的Dota2攻略小技能,希望能帮助每个萌新下分,也希望能有越来越多的人帮忙完善更新,希望Make CNDOTA Great Again。PS:作为毒瘤冰女玩家,我夹带了冰女私货攻略。触发词:dota、dota2、dota攻略、dota出装、出装建议、dota打法、dota技巧。版权所有——六分仪,可以转载迭代,请标注初代原作者。
metadata: { "openclaw": { "emoji": "⚔️" } }
---
# Dota 2 出装与打法攻略
> **数据策略**:默认读本地数据库,不请求任何外部 API。用户主动要求更新时,才拉取新数据。
> **版本基准**:7.41(当前正式版)
---
## 🗄️ 本地数据库
| 文件 | 内容 | 用途 |
|------|------|------|
| `scripts/heroes_db.json` | 127英雄,属性/胜率/出场率/预警标记 | 查英雄基础数据、胜率趋势 |
| `scripts/items_db.json` | 476物品,丰富字段(description/lore/cooldown/icon/中立标记) | 查物品详情、区分中立装备 |
| `scripts/abilities_db.json` | 127英雄技能,丰富字段(description/behavior/cast_range/scepter升级) | 查技能完整数据 |
| `scripts/item_popularity.json` | 各英雄各阶段热门出装统计 | 核心出装参考 |
| `scripts/neutral_items.json` | 49个中立装备(Tiers 1-5) | 查中立装备 |
---
## 📝 输出格式
**模块排序(由上至下)**:
1. 胜率(始终)
2. 英雄概况(按需附加)
4. 技能数据(按需附加)
5. 天赋树(按需附加)
6. 对线思路(按需附加,当问题涉及对线英雄时展示)
7. 常规出装(始终)
8. 思路说明(始终)
9. 打法要点(始终)
**按需附加说明**:英雄概况/技能数据/天赋树/对线思路**仅在用户明确要求时**才输出(如「给我看看技能数据」「介绍一下这个英雄」),分段胜率默认固定展示中军(可按需问其他段,如「传奇段胜率」「超凡入圣段胜率」)。
---
## 📋 模块格式模板
```
⚔️ 英雄名 [位置]
━━━ 胜率 ━━━
| 指标 | 全局 | 变化 | 中军 | ← 默认展示分段,可按需问其他段
|------|------|------|------|
| 胜率 | xx.x% ⚡ | (+/-x.x%) | xx.x% |
| 出场率 | x.xx% | (+/-x.xx%) | x.xx% |
━━━ 英雄概况 ━━━
- **定位:** Carry / Support / Nuker / ...
- **攻击/移速/护甲:** 近战/远程 | xxx 移速 | xx 护甲
- **属性成长:** 力量 xx(+x.x)| 敏捷 xx(+x.x)| 智力 xx(+x.x)
━━━ 技能数据 ━━━
| 按键 | 英文名 | 耗蓝 | 冷却 |
|------|--------|------|------|
| Q | xxx | xxx | xxx |
━━━ 天赋树 ━━━
| 等级 | 左 | 右 |
|------|----|----|
| 25 | xxx | xxx |
━━━ 对线思路 ━━━
(仅当问题涉及对线英雄时展示)
- **对位英雄:** 对方英雄名 + 简要威胁分析
- **我方优势:** 我方英雄的优势点
- **反制手段:** 针对对方技能的应对策略
- **注意事项:** 对线禁忌或关键细节
━━━ 常规出装 ━━━
| 阶段 | 推荐出装 | 热门出装 |
|------|---------|---------|
| 出门 | xxx | xxx |
| 前期 | xxx | xxx |
| 中期 | xxx | xxx |
| 后期 | xxx | xxx |
> ⚠️ **装备名称不带百分比/数字**,只写物品名称,如:`动力鞋` / `黯灭` / `蝴蝶`
━━━ 思路说明 ━━━
- **锁定:** 根据定位和 item_popularity 生成核心出装路线
- **局势调整:** 对方___ → 换___;对方___ → 换___
- **核心思路:** 简述该英雄的出装核心理念和节奏
━━━ 打法要点 ━━━
- **技能加点:** 主升技能 + 副升技能
- **对线:** 对线策略和注意事项
- **游走:** 游走时机和目标
- **打团:** 打团定位和目标选择
- **节奏:** 关键时间节点和节奏把控
```
**标记规则**:
- **胜率相关**:✅ 前20%(高)| (无标记)20-80% | ⚡ 后20%(低)| ⚠️ 后10%(极低)
- **变化相关**:✅ 前20%(上升多/跌得少)| (无标记)| ⚡ 后20%(跌得多)| ⚠️ 后10%(暴跌)
---
## 🧠 出装生成逻辑
> 对任意英雄,循以下步骤生成出装建议:
### 第一步:查 item_popularity
根据英雄 hero_key 查 item_popularity.json,各阶段 top3 → 填入"热门出装"列。
### 第二步:特殊英雄判断
**水晶室女 Crystal Maiden(CM)** 使用固定出装路线,不走通用逻辑:
- 出门:大魔棒 + 绿鞋 + 满血药水 + 树之祭祀
- 15 分钟:魔晶(准时出,质变装)
- 20-22 分钟:跳刀
- 28-30 分钟:A杖
- 36 分钟:BKB
- 根据局势:永恒之盘 / 希瓦之守护(替代 BKB 或其后补)
- 辅助装全程正常购买(眼、雾、粉、真假眼)
**其他英雄**:走通用逻辑,从 item_popularity 读取热门出装。
### 第三步:选推荐出装
从 top3 热门中选最具代表性的 → 填入"推荐出装"列。(CM 不执行此步)
> **过滤原则**:生成推荐出装时,先排除该英雄在**任意阶段 top3 热门中从不出或极少出现(<5%)**的装备(如 AM 不出蝴蝶,QOP 不出跳刀),确保推荐与热门数据一致、自洽。
### 第四步:生成思路说明
- **锁定:** 根据定位(Carry/Support/Nuker等)+ item_popularity 生成核心路线
- **局势调整:** 查局势出装逻辑表,根据对方阵容特征生成替换建议
- **核心思路:** 简述该英雄的出装核心理念
### 第五步:打法要点
根据英雄定位和技能特点生成五项打法要点。
---
## 📦 局势出装逻辑
| 对方阵容特征 | 优先装备 | 原因 |
|------------|---------|------|
| 法系爆发多(骨法/蓝猫/女王/宙斯) | 笛子 / 黑皇杖 | 魔抗或魔免 |
| 物理核顺(PA/小鱼/巨魔/斯拉克) | 推推棒 / 吹风 | 拉扯/保命 |
| 控制多先手强(抄袭/谜团/猛犸) | 吹风 / 跳刀 | 自保反手 |
| 有强力奶/回复(陈/戴泽/尸王) | 紫苑 / 笛子 | 限制治疗 |
| 需要推塔滚雪球(先知/光法/推塔阵容) | 黯灭 / 飓风长戟 | 强化推进 |
| 大优碾压(我方领先2-3人头以上) | 刃甲 / 跳刀 | 主动开团 |
**附加规则**:
- 对方有紫苑/大根等高爆发 → 优先BKB
- 对方有强沉默(天灾/赏金/森海) → 优先林肯
- 对方护盾多(哈斯卡/敌法/伐木机) → 黯灭优先
- 我方缺控制 → 跳刀/微光/阿托斯/缚神锁优先(跳刀硬控;微光救人;阿托斯单体缚锁;缚神锁范围缚锁+闪电链)
- 对方突脸多(PA/小鱼/混沌) → 吹风/推推保命
- 对线压力大手长远程(sniper/火枪/黑鸟) → 撑血/护腕/绿鞋
---
## 🔧 特殊装备适用场景
> 某些装备具有独特机制或质变效果,按场景整理,辅助判断何时选择。
### 硬控与开团
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **跳刀** | 瞬间位移+硬控 | 先手/逃命/追击,核心节奏装 |
| **阿托斯** | 单体缚锁(root)对线强 | 缺控制的辅助或劣单 |
| **缚神锁** | 范围缚锁+闪电链 | 缺控制的辅助,团战控场 |
| **深渊之刃** | 范围晕眩+输出 | 劣单/辅助需要硬控时 |
| **邪恶镰刀** | 单体羊,最强单体控制 | 对方有核心需要秒,或我方缺硬控 |
### 法术与爆发
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **血棘** | 暴击+法术增强 | 物理核心需要秒人,叠加法术伤害 |
| **达贡之神力** | 法系爆发滚雪球 | INT 英雄顺风时补伤害 |
| **风之杖** | 位移+治疗+驱散 | 需要保命/反手,或对方有强沉默 |
### 攻速与暴击
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **蝴蝶** | 敏捷+闪避+攻速,全面提升 | 物理核心的标准输出装 |
| **金箍棒** | 真击+伤害,克制闪避 | 对方有蝴蝶/剑舞等闪避装备 |
| **银月之晶** | 攻速叠加,可队友使用 | 后期物理核攻速不足时的补充 |
| **雷神之锤** | 链式闪电+攻速 | 推进/分裂刹/对线压制 |
### 特效功能装
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **虚灵之刃** | 灵化+爆发,克制物理核 | 对方物理核顺,需要反制 |
| **散魂剑** | 净化+减速,克制buff | 对方有薄葬/冲拳等buff技能 |
| **绝刃** | 攻击+生命偷取+减速 | 对方有强回复或需要持续输出 |
| **怪蛇之息** | 法术伤害+减速 | 对方有一定魔抗,需要持续法术压制 |
### 防御装
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **恐鳌之心** | 大量 HP,力量英雄核心 | 对方物理爆发高,力量核需要站桩 |
| **强袭胸甲** | 护甲光环+攻速 | 辅助需要光环,或推进阵容 |
| **林肯法球** | 抵挡单体技能 | 对方有单体指向性技能(莱恩戳/船长枪等) |
| **黑皇杖** | 魔免,核心保命 | 对方法术爆发高,或需要站桩输出 |
### 辅助与功能
| 装备 | 特点 | 何时出 |
|------|------|--------|
| **卫士胫甲** | 光环治疗+驱散 | 辅助核心装,团队缺奶时优先 |
| **希瓦的守护** | 冰环减速+护甲 | 劣单/辅助需要范围减速 |
| **飓风长戟** | 远程推人+撑血 | 智力英雄需要机动性,或对线压制远程 |
| **阿哈利姆神杖** | 技能强化 | 特定英雄(如莱恩/bane)质变装 |
---
## 🎯 针对性出装
> 核心原则:先判断己方定位,再选择对应克制装备。不是每个人都出,是谁适合出谁出。
> ⚠️ **注意**:Dota2 频繁更新,英雄/装备改动后需重新分析,本指南基于 7.41 版本。
### 出装定位原则
| 定位 | 出装方向 | 核心逻辑 |
|------|---------|---------|
| **1/2号核心** | 配合自身输出打克制 | 黯灭/大隐刀/否决/金箍棒 |
| **3号位** | 撑血/护甲/站桩 | 恐鳌之心/强袭胸甲/西瓦/黑皇杖 |
| **4/5号辅助** | 保护队友/视野/救人 | 微光披风/卫士胫甲/林肯/风之杖 |
### 克制出装(按定位分)
**对方有闪避(PA/小黑/蝴蝶)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 核心(1/2) | **金箍棒** | 真击必中,配合自身输出刹掉 |
**对方有隐身英雄**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **粉末** | 成本低,辅助常带 |
| 核心(1/2) | **银霉之锋** | 本身是输出装,带破隐被动 |
**对方有高回复(WD/戴泽/尸王/NEC/潮汐/钢背)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 核心(1/2) | **否决坠饰** | 破坏回复+减速,核心出有伤害 |
| 辅助(4/5) | **魂之瓮** | 辅助常备,功能装便宜 |
| 3号位 | **强袭胸甲** | 光环压护甲,压制治疗效果 |
**对方有护盾(哈斯卡/敌法/伐木机)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 核心(1/2) | **黯灭** | 削护甲,克制护盾 |
**对方法系爆发高(骨法/蓝猫/宙斯)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **笛子** | 团队魔抗,辅助出光环收益大 |
| 核心(1/2) | **黑皇杖** | 站桩输出的核心需要魔免 |
**对方物理核顺(PA/小鱼/巨魔)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **微光披风** / **吹风** | 救人保人,辅助本职 |
| 核心(1/2) | **推推棒** | 拉扯自保,不影响输出节奏 |
**对方强先手控制(抄袭/谜团/猛犸)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **微光披风** | 救人挡先手,辅助本职 |
| 核心(1/2) | **林肯法球** | 挡住一次关键控制,保证输出环境 |
**对方推进阵容(先知/光法)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **笛子** / **战鼓** | 团队光环装,辅助出全队受益 |
**对方有强单体指向(莱恩/船长/军团/剑圣/路西法)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **林肯法球** | 帮被点名的核心挡一次 |
| 核心(1/2) | **吹风** / **推推棒** | 自保,拉开距离反打 |
**对方有强沉默(天灾/赏金/森海)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 辅助(4/5) | **风之杖** | 驱散沉默,辅助需要不被沉默 |
**对方有强被动(NEC/哈斯卡/小鱼/军团/小鹿/潮汐)**
| 谁出 | 装备 | 说明 |
|------|------|------|
| 核心(1/2) | **大隐刀** / **否决坠饰** | 核心出才有伤害,破坏被动让对方无法站桩 |
---
## 🗡️ 英雄核心装
> 以下英雄的出装有较强固定性,核心装备绕不开。
### 核心大件
| 英雄 | 核心装(按顺序) | 说明 |
|------|----------------|------|
| 敌法师 Anti-Mage | 狂战斧 |farm 核心,刷钱装,无脑优先 |
| 斧王 Axe | 跳刀 + 恐鳌之心 | 跳刀先手,恐鳌站桩 |
| 冥界亚龙 Viper | 飓风长戟 + 黑皇杖 | 粘人输出,后期站桩 |
| 巨魔战将 Troll Warlord | 幻影斧 + 深渊之刃 | 站桩输出,团战无敌 |
| 混沌骑士 Chaos Knight | 恐鳌之心 / 深渊之刃 | 力量核,实体需要硬度 |
| 闪电幽魂 Spectre | 幻影斧 + 恐鳌之心 | 幻象核,需要肉装支撑 |
| 变体精灵 Morphling | 虚灵之刃 / 撒旦之邪力 | 灵化形态或力量形态,根据局势选 |
### 特定辅助装
| 英雄 | 核心装 | 说明 |
|------|--------|------|
| 巫医 Witch Doctor | 卫士胫甲 + 梅克斯 | 团战奶妈,光环装 |
| 术士 Warlock | 阿哈利姆神杖 | 地狱火质变,团控 |
| 暗影萨满 Shadow Shaman | 跳刀 + 缚神锁/邪恶镰刀 | 超远控制链 |
| 精灵守护者 Keeper of the Light | 魂之瓮 + 卫士胫甲 | 治疗削弱+团队光环 |
| 全能骑士 Omniknight | 卫士胫甲 + 强袭胸甲 | 保护核心,光环团队 |
---
## 🎮 游戏核心机制
| 机制 | 说明 |
|------|------|
| 魔晶 | 只能对 **15分钟前未购买** 的装备生效(7.41) |
| 肉山 | 白天在上路河道,黑夜在下路河道 |
| 经验符 | 每 7 分钟刷新(整分钟) |
| 强化符 | 每 2 分钟刷新(整分钟) |
| 赏金符 | 每 4 分钟刷新(整分钟) |
| 野怪刷新 | 整分钟刷新(1:00, 2:00...) |
| 兵线刷新 | 半分钟刷新(0:30, 1:30...) |
---
## 🗂️ 数据更新方法
### 1. abilities_db.json 和 items_db.json(一键,来源:dotabase)
```bash
# 下载 dotabase 最新数据
curl -sL "https://raw.githubusercontent.com/mdiller/dotabase/master/json/abilities.json" -o /tmp/dotabase_abilities.json
curl -sL "https://raw.githubusercontent.com/mdiller/dotabase/master/json/items.json" -o /tmp/dotabase_items.json
# 合并到本地数据库(abilities 保留本地 talents 文字,items 保留本地 key)
python3 scripts/merge_abilities.py
python3 scripts/merge_items.py
```
> **说明**:abilities 和 items 数据来源于 [mdiller/dotabase](https://github.com/mdiller/dotabase),包含 description、behavior、icon 等丰富字段。talent 树保留本地原始数据(dotabase 不含文字描述)。
### 2. heroes_db.json + item_popularity + 分段胜率(一键,来源:OpenDota)
```bash
# 拉取胜率/出场率数据 + 热门出装 + 分段胜率
python3 scripts/update_market_share.py && python3 scripts/update_hero_warnings.py
python3 scripts/fetch_item_popularity.py
python3 scripts/fetch_bracket_wr.py
```
### 3. 中文名(来源:Valve API)
```bash
python3 scripts/fetch_hero.py --cn
```
> 以上两步(2+3)约 5-8 分钟,可根据需要单独运行。
---
## 📅 更新记录
- 2026-04-23:首次构建本地数据库版 skill
- 2026-04-23 晚:精简 skill;基础模块改为4个;局势调整并入思路说明
- 2026-04-23 晚:常规属性并入英雄概况(方案C三行式);调整模块排序;美化模板格式
FILE:README.md
# Dota 2 出装与打法攻略
> 跟风使用openclaw,但是不知道都能做什么,心血来潮搜了一下居然没有dota2相关skill,决定自己动手,所以这只是一个先锋分段萌新抛砖引玉自制skill,希望大家批评指正,希望大家一起更新,未来也许ai能普及,希望那个时候这个skill已经完备到萌新碰到一个新英雄就可以学习的程度,不是简单的查表,而是ai的分析,也希望Make CNDOTA Great Again!
⚔️ **触发词**:dota、dota2、dota攻略、dota出装、出装建议、dota打法、dota技巧、xx怎么出装
---
## 一、这是什么
Dota 2 攻略skill,专注于出装推荐和打法思路。
数据基于 OpenDota 统计和 dotabase 游戏文件,本地存储,按需读取,不依赖外部 API。
---
## 二、使用方法
### 触发方式
直接发送触发词即可,例如:
- "火枪怎么出装"
- "影魔中单怎么打"
- "小黑统帅段位胜率"
### 输出模块(按顺序)
1. **胜率** — 全局 + 中军分段(默认),问其他分段如"传奇段胜率"会替换显示
2. **常规出装** — 推荐出装 + 热门出装(按阶段)
3. **思路说明** — 出装逻辑和局势调整
4. **打法要点** — 技能加点、对线、游走、打团、节奏
### 可按需询问(单独触发)
- `英雄概况` — 定位、属性、攻击类型等
- `技能数据` — 各技能 CD、耗蓝、描述
- `天赋树` — 各级天赋选择
- `对线思路` — 针对特定对线英雄的策略
- `某个段位的胜率` — 如"火枪在传奇段胜率怎么样"
### 其他问题
- 根据提问可以展示数据库已有内容,可以基于装备和技能数据给出分析。(bata阶段)
---
## 三、数据更新
### 数据更新方式
- 目前还需要手动更新,已经在skill中写好了拉取方式,也写好了脚本,但是需要手动让agent拉取,有能力的可以试着设计cron,我的虾cron各种出问题,所以不定期手动更新了。
- 数据来源都是公开api,也是慢慢找到。
### 数据来源
| 数据 | 来源 | 更新频率 |
|------|------|---------|
| 英雄基础属性/胜率/出场率 | OpenDota heroStats API | 按需拉取 |
| 热门出装统计 | OpenDota item_popularity | 按需拉取 |
| 分段胜率(8个段位) | OpenDota heroStats API | 按需拉取 |
| 技能详细数据 | dotabase (GitHub) | 按需拉取 |
| 物品详细数据 | dotabase (GitHub) | 按需拉取 |
| 中文名 | Valve API | 按需拉取 |
---
## 四、更新日志
### v1.1.3
- 新增:分段胜率模块,默认展示中军(可按需问其他段位)
- 新增:`fetch_bracket_wr.py` 脚本,从 OpenDota 拉取各分段胜率和选择率
- 数据:heroes_db.json 新增 `bracket_wr` 字段
### v1.1.2
- 数据:abilities_db.json 和 items_db.json 合并 dotabase 丰富字段
- 新增:技能 description、behavior、cast_range;物品 description、lore、cooldown、is_neutral_enhancement
- 新增:`merge_abilities.py`、`merge_items.py` 合并脚本
- 删除:旧 `update_abilities.py`
### v1.1.1
- 精简 skill 结构,移除冗余模块
- 局势调整并入思路说明
- 增加部分数据抓取方式,持续更新数据库
### v1.1.0
- 首次发布至 ClawHub
- 基于 OpenDota 数据重新构建本地数据库,明确数据及展示规则
- 固化显示模板。
### v1.0.0
- 初步设计展示模板。
- 通过公开api新建本地数据库
### v0.0.1
- 2026年4月23日,首次创建项目,在没有任何数据支持的情况下,触发分析。
---
版权所有 — 六分仪
欢迎大家转载迭代,建立分支,请保留并标注初代原作者
FILE:_meta.json
{
"ownerId": "kn757aerhmnk033zv2qrsf29wn85dnch",
"slug": "dota2-coach",
"version": "1.0.0",
"publishedAt": 1776965321997
}
FILE:scripts/fetch_bracket_wr.py
#!/usr/bin/env python3
"""
从 OpenDota heroStats API 拉取分段胜率+选择率
并入 heroes_db.json
分段选择率 = 该英雄在该分段的pick / 该分段所有英雄总pick
分段映射(中国服):
1: 先锋 / 2: 卫士 / 3: 中军 / 4: 统帅
5: 传奇 / 6: 万古流芳 / 7: 超凡入圣 / 8: 冠绝
用法: python3 fetch_bracket_wr.py
"""
import json, urllib.request
SCRIPT_DIR = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts'
HEROES_FILE = f'{SCRIPT_DIR}/heroes_db.json'
BRACKET_NAMES = {
1: '先锋', 2: '卫士', 3: '中军', 4: '统帅',
5: '传奇', 6: '万古流芳', 7: '超凡入圣', 8: '冠绝',
}
url = 'https://api.opendota.com/api/heroStats'
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
# 先算各分段的总pick(所有英雄pick之和)
bracket_totals = {i: 0 for i in range(1, 8)}
for entry in data:
for i in range(1, 8):
pick_key = f'{i}_pick'
if pick_key in entry:
bracket_totals[i] += entry[pick_key]
with open(HEROES_FILE) as f:
heroes = json.load(f)
hero_by_key = {h['key']: h for h in heroes}
for entry in data:
name = entry.get('name', '')
key = name.replace('npc_dota_hero_', '')
if key not in hero_by_key:
continue
bracket_data = {}
for i in range(1, 8):
pick_key = f'{i}_pick'
win_key = f'{i}_win'
if pick_key in entry and win_key in entry:
picks = entry[pick_key]
wins = entry[win_key]
wr = round(wins / picks * 100, 1) if picks > 0 else 0
share = round(picks / bracket_totals[i] * 100, 2) if bracket_totals[i] > 0 else 0
bracket_data[BRACKET_NAMES[i]] = {'wr': wr, 'picks': picks, 'share': share}
hero_by_key[key]['bracket_wr'] = bracket_data
with open(HEROES_FILE, 'w') as f:
json.dump(heroes, f, ensure_ascii=False, indent=2)
with open(HEROES_FILE) as f:
heroes = json.load(f)
antimage = next(h for h in heroes if h['key'] == 'antimage')
zhongjun = antimage['bracket_wr']['中军']
print(f"antimage 中军: wr={zhongjun['wr']}%, picks={zhongjun['picks']}, share={zhongjun['share']}%")
print(f"Total heroes: {len([h for h in heroes if 'bracket_wr' in h])}")
FILE:scripts/fetch_hero.py
#!/usr/bin/env python3
"""
Dota 2 英雄+物品综合查询脚本
使用本地 JSON 数据库 + OpenDota API 实时数据
版本:7.41 | 最后更新:2026-04-23
"""
import sys
import json
import os
import urllib.request
import urllib.error
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def load_json(filename):
path = os.path.join(SCRIPT_DIR, filename)
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return []
def get_heroes_from_api():
url = "https://api.opendota.com/api/heroStats"
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return json.loads(resp.read())
except Exception as e:
print(f"[API] 获取英雄数据失败: {e}", file=sys.stderr)
return None
def get_items_from_api():
url = "https://api.opendota.com/api/constants/items"
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return json.loads(resp.read())
except Exception as e:
print(f"[API] 获取物品数据失败: {e}", file=sys.stderr)
return None
def update_heroes_db():
"""更新英雄数据库(从 OpenDota)"""
data = get_heroes_from_api()
if data is None:
print("[错误] 无法从API获取英雄数据")
return False
heroes = []
for h in data:
heroes.append({
'id': h['id'],
'name': h['localized_name'],
'key': h['name'].replace('npc_dota_hero_',''),
'roles': h.get('roles',[]),
'attr': h.get('primary_attr',''),
'attack': h.get('attack_type',''),
'str': h.get('base_str',0),
'agi': h.get('base_agi',0),
'int': h.get('base_int',0),
'str_gain': h.get('str_gain',0),
'agi_gain': h.get('agi_gain',0),
'int_gain': h.get('int_gain',0),
'move_speed': h.get('move_speed',0),
'armor': h.get('base_armor',0),
'attack_range': h.get('attack_range',0),
'hp': h.get('base_health',0),
'mana': h.get('base_mana',0),
'damage_min': h.get('base_attack_min',0),
'damage_max': h.get('base_attack_max',0),
'pub_pick': h.get('pub_pick',0),
'pub_win': h.get('pub_win',0),
'pro_pick': h.get('pro_pick',0),
'pro_win': h.get('pro_win',0),
'1_pick': h.get('1_pick',0), '1_win': h.get('1_win',0),
'2_pick': h.get('2_pick',0), '2_win': h.get('2_win',0),
'3_pick': h.get('3_pick',0), '3_win': h.get('3_win',0),
'4_pick': h.get('4_pick',0), '4_win': h.get('4_win',0),
'5_pick': h.get('5_pick',0), '5_win': h.get('5_win',0),
})
path = os.path.join(SCRIPT_DIR, "heroes_db.json")
with open(path, 'w') as f:
json.dump(heroes, f, ensure_ascii=False)
print(f"英雄数据库已更新: {len(heroes)} 个英雄")
return True
def update_cn_names():
"""从 dota2.com.cn 获取英雄和物品的中文名"""
heroes_path = os.path.join(SCRIPT_DIR, "heroes_db.json")
items_path = os.path.join(SCRIPT_DIR, "items_db.json")
# Fetch 英雄中文名
try:
req = urllib.request.Request(
"https://www.dota2.com.cn/datafeed/heroList?task=herolist",
headers={"Referer": "https://www.dota2.com.cn/", "User-Agent": "Mozilla/5.0"}
)
with urllib.request.urlopen(req, timeout=15) as resp:
cn_heroes_data = json.loads(resp.read())['result']['heroes']
print(f"[CN] 获取到 {len(cn_heroes_data)} 个英雄中文名")
except Exception as e:
print(f"[CN] 获取英雄中文名失败: {e}")
cn_heroes_data = []
# Fetch 物品中文名
try:
req = urllib.request.Request(
"https://www.dota2.com.cn/datafeed/itemlist?task=itemlist",
headers={"Referer": "https://www.dota2.com.cn/", "User-Agent": "Mozilla/5.0"}
)
with urllib.request.urlopen(req, timeout=15) as resp:
cn_items_data = json.loads(resp.read())['result']['data']['itemabilities']
print(f"[CN] 获取到 {len(cn_items_data)} 个物品中文名")
except Exception as e:
print(f"[CN] 获取物品中文名失败: {e}")
cn_items_data = []
# Build CN maps
cn_hero_map = {h['name']: h['name_loc'] for h in cn_heroes_data}
cn_item_map = {}
for item in cn_items_data:
key = item['name'].replace('item_', '') if item['name'].startswith('item_') else item['name']
cn_item_map[key] = item['name_loc']
# Update heroes_db
if os.path.exists(heroes_path):
with open(heroes_path) as f:
heroes = json.load(f)
updated = 0
for hero in heroes:
for cn_h in cn_heroes_data:
if cn_h['name_english_loc'] == hero['name']:
hero['localized_name'] = cn_h['name_loc']
updated += 1
break
with open(heroes_path, 'w') as f:
json.dump(heroes, f, ensure_ascii=False, indent=2)
print(f"[CN] 英雄中文名已更新: {updated}/{len(heroes)} 个")
# Update items_db
if os.path.exists(items_path):
with open(items_path) as f:
items = json.load(f)
updated = 0
for item in items:
key = item.get('key', '')
if key in cn_item_map:
item['localized_name'] = cn_item_map[key]
updated += 1
with open(items_path, 'w') as f:
json.dump(items, f, ensure_ascii=False, indent=2)
print(f"[CN] 物品中文名已更新: {updated}/{len(items)} 个")
return True
def update_items_db():
"""更新物品数据库"""
data = get_items_from_api()
if data is None:
print("[错误] 无法从API获取物品数据")
return False
items = []
for k,v in data.items():
if isinstance(v,dict) and v.get('id'):
items.append({
'id': v['id'],
'key': k,
'name': v.get('dname',''),
'cost': v.get('cost',0),
'qual': v.get('qual',''),
})
items.sort(key=lambda x: x['id'])
path = os.path.join(SCRIPT_DIR, "items_db.json")
with open(path, 'w') as f:
json.dump(items, f, ensure_ascii=False)
print(f"物品数据库已更新: {len(items)} 个物品")
return True
def search_heroes(query, heroes):
q = query.lower()
results = []
for h in heroes:
if q in h['name'].lower() or q in h.get('key','').lower() or str(h['id']) == q:
results.append(h)
return results
def search_items(query, items):
q = query.lower()
results = []
for it in items:
if q in it['name'].lower() or q in it.get('key','').lower() or str(it['id']) == q:
results.append(it)
return results
def print_hero(h):
print(f"\n{'='*50}")
print(f"📊 {h['name']} (ID:{h['id']})")
print(f"定位: {', '.join(h.get('roles', []))}")
print(f"属性: {h.get('attr','')} / {h.get('attack','')}")
print(f"基础属性: STR {h.get('str',0)}(+{h.get('str_gain',0)}) / AGI {h.get('agi',0)}(+{h.get('agi_gain',0)}) / INT {h.get('int',0)}(+{h.get('int_gain',0)})")
print(f"移速: {h.get('move_speed',0)} | 护甲: {h.get('armor',0)} | 攻击范围: {h.get('attack_range',0)}")
for pos, name in [(1,'Carry'),(2,'Mid'),(3,'Offlane'),(4,'SoftSupport'),(5,'HardSupport')]:
pick = h.get(f'{pos}_pick', 0)
if pick > 1000:
win = h.get(f'{pos}_win', 0)
print(f"{name}: {pick}场 胜率 {win/max(pick,1)*100:.1f}%")
pub_pick = h.get('pub_pick', 0)
pub_win = h.get('pub_win', 0)
if pub_pick > 0:
print(f"路人总: {pub_pick}场 胜率 {pub_win/max(pub_pick,1)*100:.1f}%")
pro_pick = h.get('pro_pick', 0)
pro_win = h.get('pro_win', 0)
if pro_pick > 0:
print(f"职业赛: {pro_pick}场 胜率 {pro_win/max(pro_pick,1)*100:.1f}%")
print(f"{'='*50}")
def print_item(it):
cost = it.get('cost', 0)
print(f" {it['name']} (ID:{it['id']}) | 价格:{cost} | 品质:{it.get('qual','')}")
if __name__ == "__main__":
heroes = load_json("heroes_db.json")
items = load_json("items_db.json")
if len(sys.argv) < 2:
print("用法: python3 fetch_hero.py <命令> [参数]")
print(" python3 fetch_hero.py crystal 查询英雄")
print(" python3 fetch_hero.py blink 查询物品")
print(" python3 fetch_hero.py --update 更新数据库(OpenDota)")
print(" python3 fetch_hero.py --cn 更新中文名(dota2.com.cn)")
print(" python3 fetch_hero.py --list 列出所有英雄")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "--update":
update_heroes_db()
update_items_db()
elif cmd == "--cn":
update_cn_names()
elif cmd == "--list":
if not heroes:
print("[错误] 未找到heroes_db.json")
sys.exit(1)
print(f"\n共 {len(heroes)} 个英雄:\n")
for h in sorted(heroes, key=lambda x: x['id']):
print(f" {h['id']:3d}. {h['name']}")
elif len(sys.argv) > 2 and sys.argv[1] == "--hero":
query = " ".join(sys.argv[2:])
if not heroes:
print("[错误] 未找到heroes_db.json,请先运行 --update")
sys.exit(1)
results = search_heroes(query, heroes)
if results:
for h in results:
print_hero(h)
else:
print(f"未找到英雄: {query}")
elif len(sys.argv) > 2 and sys.argv[1] == "--item":
query = " ".join(sys.argv[2:])
if not items:
print("[错误] 未找到items_db.json,请先运行 --update")
sys.exit(1)
results = search_items(query, items)
if results:
for it in results[:10]:
print_item(it)
else:
print(f"未找到物品: {query}")
else:
# 通用搜索
query = " ".join(sys.argv[1:])
if not heroes or not items:
print("[错误] 数据库未初始化,请先运行 --update")
sys.exit(1)
hero_results = search_heroes(query, heroes)
item_results = search_items(query, items)
if hero_results:
print(f"\n英雄匹配 '{query}':")
for h in hero_results[:5]:
print_hero(h)
if item_results:
print(f"\n物品匹配 '{query}':")
for it in item_results[:10]:
print_item(it)
if not hero_results and not item_results:
print(f"未找到: {query}")
FILE:scripts/fetch_item_popularity.py
#!/usr/bin/env python3
"""
抓取每个英雄的装备选择率数据
保存至 item_popularity.json
"""
import json, time, subprocess, os, sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ITEMS_FILE = f'{SCRIPT_DIR}/items_db.json'
HEROES_FILE = f'{SCRIPT_DIR}/heroes_db.json'
OUTPUT_FILE = f'{SCRIPT_DIR}/item_popularity.json'
def load_heroes():
with open(HEROES_FILE) as f:
return json.load(f)
def load_items():
with open(ITEMS_FILE) as f:
items = json.load(f)
return {item['id']: item for item in items}
def fetch_item_popularity(hero_id):
url = f'https://api.opendota.com/api/heroes/{hero_id}/itemPopularity'
try:
result = subprocess.run(
['curl', '-sL', url, '-H', 'User-Agent: Mozilla/5.0', '--max-time', '15'],
capture_output=True, text=True, timeout=20
)
if result.returncode == 0 and result.stdout.strip():
return json.loads(result.stdout)
except Exception as e:
print(f' [ERROR] Hero {hero_id}: {e}', file=sys.stderr)
return None
def main():
heroes = load_heroes()
id_to_item = load_items()
# Load existing
existing = {}
if os.path.exists(OUTPUT_FILE):
with open(OUTPUT_FILE) as f:
existing = json.load(f)
done_ids = set(existing.keys())
print(f'Already have: {len(done_ids)} heroes')
fetched = 0
errors = 0
for h in heroes:
hero_id = h['id']
hero_name = h.get('localized_name', h['name'])
if str(hero_id) in done_ids:
continue
print(f'Fetching {hero_name} (ID:{hero_id})...', end=' ', flush=True)
data = fetch_item_popularity(hero_id)
if data is not None:
# Convert item IDs to names/localized_names
phases = ['start_game_items', 'early_game_items', 'mid_game_items', 'late_game_items']
simplified = {}
for phase in phases:
items_raw = data.get(phase, {})
if items_raw:
simplified[phase] = {}
for item_id, count in items_raw.items():
try:
item_id_int = int(item_id)
item_info = id_to_item.get(item_id_int, {})
item_name = item_info.get('localized_name') or item_info.get('name', f'item_{item_id}')
simplified[phase][item_name] = count
except (ValueError, TypeError):
pass
existing[str(hero_id)] = {
'hero_name': hero_name,
'hero_key': h.get('key', ''),
'phases': simplified
}
fetched += 1
print(f'OK ({len(simplified)} phases)')
else:
errors += 1
print(f'FAILED')
# Save incrementally
with open(OUTPUT_FILE, 'w') as f:
json.dump(existing, f, ensure_ascii=False, indent=2)
# Delay to avoid 403
time.sleep(2.5)
print(f'\nDone. Fetched: {fetched}, Errors: {errors}, Total: {len(existing)}')
if __name__ == '__main__':
main()
FILE:scripts/merge_abilities.py
#!/usr/bin/env python3
"""
从 dotabase abilities.json 提取更丰富的技能数据
保持与原有 abilities_db.json 相同的 hero_key 结构
用法: python3 merge_abilities.py
"""
import json
DOTABASE_FILE = '/tmp/dotabase_abilities.json'
OUTPUT_FILE = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts/abilities_db.json'
HEROES_FILE = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts/heroes_db.json'
# 加载英雄映射
with open(HEROES_FILE) as f:
heroes = json.load(f)
hero_id_to_key = {h['id']: h['key'] for h in heroes}
# 加载旧的天赋数据(因为 dotabase 的 talents 没文字描述)
with open(OUTPUT_FILE) as f:
old_db = json.load(f)
old_talents_map = {h['hero_key']: h['talents'] for h in old_db}
# 加载 dotabase abilities
with open(DOTABASE_FILE) as f:
dotabase = json.load(f)
# 按 hero_id 分组
hero_abilities = {}
for ab in dotabase:
hid = ab.get('hero_id')
if not hid:
continue
key = hero_id_to_key.get(hid)
if not key:
continue
if key not in hero_abilities:
hero_abilities[key] = []
hero_abilities[key].append(ab)
# 生成新数据库
result = []
for h in heroes:
key = h['key']
abs_list = hero_abilities.get(key, [])
# 提取有 description 的主要技能
abilities = []
for ab in abs_list:
if not ab.get('description'):
continue
abilities.append({
'key': ab.get('name', ''),
'name': ab.get('localized_name', ''),
'mc': ab.get('mana_cost'),
'cd': ab.get('cooldown'),
'desc': ab.get('description', ''),
'behavior': ab.get('behavior', ''),
'cast_range': ab.get('cast_range'),
'cast_point': ab.get('cast_point'),
'duration': ab.get('duration'),
'damage': ab.get('damage'),
'scepter_grants': ab.get('scepter_grants'),
'shard_grants': ab.get('shard_grants'),
})
result.append({
'hero_key': key,
'abilities': abilities,
'talents': old_talents_map.get(key, [])
})
with open(OUTPUT_FILE, 'w') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
total_abs = sum(len(x['abilities']) for x in result)
print(f'Updated {len(result)} heroes, total abilities: {total_abs}')
FILE:scripts/merge_items.py
#!/usr/bin/env python3
"""
合并 dotabase items.json 和本地 items_db.json
- dotabase 提供 description、lore、cooldown、icon 等丰富字段
- 本地提供 key(程序化ID)、qual、notes
用法: python3 merge_items.py
"""
import json
DOTABASE_FILE = '/tmp/dotabase_items.json'
LOCAL_FILE = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts/items_db.json'
OUTPUT_FILE = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts/items_db.json'
# 加载本地 items(用于获取 key、qual、notes)
with open(LOCAL_FILE) as f:
local_items = json.load(f)
# 建立 name -> local_item 的映射
local_by_name = {item['name']: item for item in local_items}
# 加载 dotabase items
with open(DOTABASE_FILE) as f:
dotabase_items = json.load(f)
# 合并
merged = []
for item in dotabase_items:
name = item.get('name', '')
local = local_by_name.get(name, {})
# 构建 key(从 name 推导,比如 "Black King Bar" -> "black_king_bar")
key = local.get('key', '')
if not key:
# 从 name 推导
key = name.lower().replace(' ', '_').replace("'", '').replace('-', '_')
merged.append({
'id': item.get('id'),
'key': key,
'name': name,
'localized_name': item.get('localized_name', ''),
'cost': item.get('cost'),
'qual': local.get('qual', ''),
'notes': local.get('notes', ''),
'description': item.get('description', ''),
'lore': item.get('lore', ''),
'cooldown': item.get('cooldown'),
'mana_cost': item.get('mana_cost'),
'cast_range': item.get('cast_range'),
'neutral_tier': item.get('neutral_tier'),
'is_neutral_enhancement': item.get('is_neutral_enhancement', False),
'secret_shop': item.get('secret_shop', False),
'shop_tags': item.get('shop_tags', ''),
})
# 按 id 排序
merged.sort(key=lambda x: x.get('id', 0))
with open(OUTPUT_FILE, 'w') as f:
json.dump(merged, f, ensure_ascii=False, indent=2)
neutral = sum(1 for x in merged if x.get('is_neutral_enhancement'))
print(f'Merged {len(merged)} items ({neutral} neutral items)')
FILE:scripts/neutral_items.json
{"2": [{"id": 71, "key": "poor_mans_shield", "name": "Poor Man's Shield", "cost": 0}, {"id": 187, "key": "medallion_of_courage", "name": "Medallion of Courage", "cost": 0}, {"id": 359, "key": "essence_ring", "name": "Essence Ring", "cost": 0}, {"id": 840, "key": "pogo_stick", "name": "Tumbler's Toy", "cost": 0}, {"id": 945, "key": "seeds_of_serenity", "name": "Seeds of Serenity", "cost": 0}, {"id": 950, "key": "defiant_shell", "name": "Defiant Shell", "cost": 0}, {"id": 1599, "key": "mana_draught", "name": "Mana Draught", "cost": 0}, {"id": 1601, "key": "crippling_crossbow", "name": "Crippling Crossbow", "cost": 0}, {"id": 1604, "key": "searing_signet", "name": "Searing Signet", "cost": 0}], "5": [{"id": 292, "key": "desolator_2", "name": "Stygian Desolator", "cost": 0}, {"id": 326, "key": "spider_legs", "name": "Spider Legs", "cost": 0}, {"id": 370, "key": "demonicon", "name": "Book of the Dead", "cost": 0}, {"id": 371, "key": "fallen_sky", "name": "Fallen Sky", "cost": 0}, {"id": 377, "key": "minotaur_horn", "name": "Minotaur Horn", "cost": 0}, {"id": 837, "key": "heavy_blade", "name": "Witchbane", "cost": 0}, {"id": 1642, "key": "dezun_bloodrite", "name": "Dezun Bloodrite", "cost": 0}, {"id": 1644, "key": "divine_regalia", "name": "Divine Regalia", "cost": 0}, {"id": 1718, "key": "riftshadow_prism", "name": "Riftshadow Prism", "cost": 0}, {"id": 1863, "key": "harmonizer", "name": "Harmonizer", "cost": 0}], "1": [{"id": 565, "key": "chipped_vest", "name": "Chipped Vest", "cost": 0}, {"id": 577, "key": "possessed_mask", "name": "Possessed Mask", "cost": 0}, {"id": 947, "key": "occult_bracelet", "name": "Occult Bracelet", "cost": 0}, {"id": 1077, "key": "dagger_of_ristul", "name": "Dagger of Ristul", "cost": 0}, {"id": 2097, "key": "duelist_gloves", "name": "Duelist Gloves", "cost": 0}, {"id": 1606, "key": "polliwog_charm", "name": "Pollywog Charm", "cost": 0}, {"id": 1637, "key": "kobold_cup", "name": "Kobold Cup", "cost": 0}, {"id": 1638, "key": "dormant_curio", "name": "Dormant Curio", "cost": 0}, {"id": 1716, "key": "weighted_dice", "name": "Weighted Dice", "cost": 0}, {"id": 1717, "key": "ash_legion_shield", "name": "Ash Legion Shield", "cost": 0}, {"id": 1861, "key": "stonefeather_satchel", "name": "Stonefeather Satchel", "cost": 0}, {"id": 1868, "key": "foragers_kit", "name": "Forager's Kit", "cost": 0}], "3": [{"id": 574, "key": "cloak_of_flames", "name": "Cloak of Flames", "cost": 0}, {"id": 675, "key": "psychic_headband", "name": "Psychic Headband", "cost": 0}, {"id": 585, "key": "stormcrafter", "name": "Stormcrafter", "cost": 0}, {"id": 1598, "key": "unrelenting_eye", "name": "Unrelenting Eye", "cost": 0}, {"id": 1603, "key": "gunpowder_gauntlets", "name": "Gunpowder Gauntlet", "cost": 0}, {"id": 1605, "key": "serrated_shiv", "name": "Serrated Shiv", "cost": 0}, {"id": 1640, "key": "jidi_pollen_bag", "name": "Jidi Pollen Bag", "cost": 0}, {"id": 1859, "key": "spellslinger", "name": "Spellslinger", "cost": 0}, {"id": 1873, "key": "partisans_brand", "name": "Partisan's Brand", "cost": 0}], "4": [{"id": 2190, "key": "dandelion_amulet", "name": "Dandelion Amulet", "cost": 0}, {"id": 1168, "key": "rattlecage", "name": "Rattlecage", "cost": 0}, {"id": 1643, "key": "giant_maul", "name": "Giant's Maul", "cost": 0}, {"id": 1719, "key": "metamorphic_mandible", "name": "Metamorphic Mandible", "cost": 0}, {"id": 1720, "key": "idol_of_screeauk", "name": "Idol of Scree'auk", "cost": 0}, {"id": 1721, "key": "flayers_bota", "name": "Flayer's Bota", "cost": 0}, {"id": 1860, "key": "prophets_pendulum", "name": "Prophet's Pendulum", "cost": 0}, {"id": 1862, "key": "enchanters_bauble", "name": "Enchanter's Bauble", "cost": 0}, {"id": 1864, "key": "conjurers_catalyst", "name": "Conjurer's Catalyst", "cost": 0}]}
FILE:scripts/update_hero_warnings.py
#!/usr/bin/env python3
"""
为每个英雄计算百分位标记并更新 heroes_db.json
百分位分级:
⚠️ 后10%(最差)
⚡ 10-20%(较差)
✅ 20-80%(正常)
📈 前10%(最强/上升最多)
自动写入 heroes_db.json 的 warn_* 字段
"""
import json, statistics
SCRIPT_DIR = '/root/.openclaw/workspace/magi/skills/dota2-coach/scripts'
HEROES_FILE = f'{SCRIPT_DIR}/heroes_db.json'
def percentile_flag(value, all_values, invert=False):
"""返回百分位标记字符串
invert=False: 值越大越好(用于变化率)→ rank = count > value(从顶计算位置)
invert=True: 值越小越好(用于胜率/出场率)→ rank = count <= value(从底计算位置)
"""
n = len(all_values)
if invert:
rank = sum(1 for v in all_values if v <= value) # 位置从底部算
else:
rank = sum(1 for v in all_values if v > value) # 位置从顶部算
pct = rank / n
if invert:
# 小值=好,pct 越低越差,pct 越高越好
if pct <= 0.1:
return 'warn_bottom10' # pct 极低 → 极差值(后10%)
elif pct <= 0.2:
return 'warn_bottom20' # pct 低 → 较差值(后20%)
elif pct >= 0.9:
return 'warn_top10' # pct 高 → 极好值(前10%)
elif pct >= 0.8:
return 'warn_top20' # pct 较高 → 较好值(前20%)
else:
# 大值=好,pct 越低越好,pct 越高越差
if pct <= 0.1:
return 'warn_top10' # pct 极低 → 极好值(前10%)
elif pct <= 0.2:
return 'warn_top20' # pct 低 → 较好值(前20%)
elif pct >= 0.9:
return 'warn_bottom10' # pct 高 → 极差值(后10%)
elif pct >= 0.8:
return 'warn_bottom20' # pct 较高 → 较差值(后20%)
return 'normal'
def warn_icon(warn_type):
"""将 warn_type 映射为显示符号"""
return {
'warn_bottom10': '⚠️',
'warn_bottom20': '⚡',
'warn_top20': '✅',
'warn_top10': '✅',
'normal': '',
'unknown': '',
}.get(warn_type, '')
def update_hero_warnings():
with open(HEROES_FILE) as f:
heroes = json.load(f)
# 计算每周全英雄总出场
weekly_totals = [0]*6
for h in heroes:
t = h.get('pub_pick_trend', [])
for i in range(min(6, len(t))):
weekly_totals[i] += t[i]
# 计算四个指标
records = []
for h in heroes:
t = h.get('pub_pick_trend', [])
w = h.get('pub_win_trend', [])
if not t or not w or len(t) < 6:
h['warn_wr'] = 'unknown'
h['warn_wr_change'] = 'unknown'
h['warn_share'] = 'unknown'
h['warn_share_change'] = 'unknown'
continue
total_pick = sum(t[:6])
total_win = sum(w[:6])
wr = total_win / total_pick * 100 if total_pick > 0 else 0
# 胜率变化:最近3周 vs 之前3周(用市场胜率而非原始胜率)
wr_old = sum(w[:3])/3 / (sum(t[:3])/3) * 100 if sum(t[:3]) > 0 else 0
wr_new = sum(w[3:6])/3 / (sum(t[3:6])/3) * 100 if sum(t[3:6]) > 0 else 0
wr_change = wr_new - wr_old
# 出场率
share = sum(t[:6]) / sum(weekly_totals) * 100
# 出场率变化
share_old = (sum(t[:3])/3) / (sum(weekly_totals[:3])/3) if sum(weekly_totals[:3]) > 0 else 0
share_new = (sum(t[3:6])/3) / (sum(weekly_totals[3:6])/3) if sum(weekly_totals[3:6]) > 0 else 0
share_change = (share_new - share_old) * 100
h['wr'] = round(wr, 1)
h['wr_change'] = round(wr_change, 1)
h['share'] = round(share, 2)
h['share_change'] = round(share_change, 2)
records.append({
'key': h.get('key', ''),
'wr': wr,
'wr_change': wr_change,
'share': share,
'share_change': share_change,
})
# 计算百分位
wr_vals = [r['wr'] for r in records]
wr_chg_vals = [r['wr_change'] for r in records]
sh_vals = [r['share'] for r in records]
sh_chg_vals = [r['share_change'] for r in records]
for h in heroes:
k = h.get('key', '')
rec = next((r for r in records if r['key'] == k), None)
if not rec:
continue
h['warn_wr'] = percentile_flag(rec['wr'], wr_vals, invert=True)
h['warn_wr_change'] = percentile_flag(rec['wr_change'], wr_chg_vals, invert=False)
h['warn_share'] = percentile_flag(rec['share'], sh_vals, invert=True)
h['warn_share_change'] = percentile_flag(rec['share_change'], sh_chg_vals, invert=False)
with open(HEROES_FILE, 'w') as f:
json.dump(heroes, f, ensure_ascii=False, indent=2)
# 统计
for field in ['warn_wr', 'warn_wr_change', 'warn_share', 'warn_share_change']:
cnt = sum(1 for h in heroes if h.get(field) == 'warn_bottom10')
print(f'{field}: {cnt} 个在后10%')
if __name__ == '__main__':
update_hero_warnings()
FILE:scripts/update_market_share.py
#!/usr/bin/env python3
"""
更新英雄数据库 + 计算市场占有率变化
每次运行自动计算出场率变化并更新 heroes_db.json
用法: python3 fetch_hero.py --update
"""
import json, subprocess, os, statistics
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HEROES_FILE = f'{SCRIPT_DIR}/heroes_db.json'
def fetch_hero_stats():
result = subprocess.run(
['curl', '-sL', 'https://api.opendota.com/api/heroStats', '-H', 'User-Agent: Mozilla/5.0'],
capture_output=True, text=True, timeout=20
)
return json.loads(result.stdout) if result.returncode == 0 else None
def update_trends_and_market_share():
"""从 OpenDota 拉数据,更新 heroes_db.json 的趋势字段和市场占有率变化"""
data = fetch_hero_stats()
if not data:
print('[ERROR] 无法获取英雄数据')
return False
with open(HEROES_FILE) as f:
heroes = json.load(f)
heroes_by_id = {h['id']: h for h in heroes}
# Update trend fields
for h in data:
hid = h['id']
if hid in heroes_by_id:
hero = heroes_by_id[hid]
hero['pub_pick_trend'] = h.get('pub_pick_trend', [])
hero['pub_win_trend'] = h.get('pub_win_trend', [])
# Calculate market share changes
num_weeks = 6
weekly_totals = [0] * num_weeks
for hero in heroes:
t = hero.get('pub_pick_trend', [])
for i in range(min(num_weeks, len(t))):
weekly_totals[i] += t[i]
for hero in heroes:
t = hero.get('pub_pick_trend', [])
if len(t) < 6:
hero['market_share_change'] = 0
continue
share_older = sum(t[:3]) / 3 / (sum(weekly_totals[:3]) / 3)
share_recent = sum(t[3:6]) / 3 / (sum(weekly_totals[3:6]) / 3)
if share_older > 0:
hero['market_share_change'] = round((share_recent - share_older) / share_older * 100, 2)
else:
hero['market_share_change'] = 0
with open(HEROES_FILE, 'w') as f:
json.dump(heroes, f, ensure_ascii=False, indent=2)
# Summary
changes = [h['market_share_change'] for h in heroes if 'market_share_change' in h]
if changes:
half_std = statistics.stdev(changes) / 2
declining = sum(1 for c in changes if c < -half_std)
rising = sum(1 for c in changes if c > half_std)
print(f'[更新完成] {len(heroes)} 个英雄')
print(f' 出场率变化标准差: {statistics.stdev(changes):.2f}%,阈值(半标准差): {half_std:.2f}%')
print(f' 上升 > {half_std:.2f}%: {rising} 个')
print(f' 下降 < -{half_std:.2f}%: {declining} 个')
return True
if __name__ == '__main__':
update_trends_and_market_share()