| name | dota2-workshop-modding |
| description | Develop Dota 2 custom games (addons) using Workshop Tools. Covers Lua VScript API, KeyValues (KV) data files, Panorama UI, Hammer map entities, and addon project structure. Use when creating or editing Dota 2 mods, custom game modes, abilities, units, items, HUD, or any Dota 2 Workshop Tools related development. |
Dota 2 Workshop Tools 开发指南
开发工作流(必读)
每次接到开发任务时,按以下顺序操作:
- 查陷阱:先查 pitfalls.md 是否有相关已知问题
- 查资料:到 wiki-links.md 查找官方 Wiki / ModDota 相关文档确认做法
- 看模板:参考 barebones 模板或 Element-TD 等开源项目的实现(链接见 wiki-links.md)
- 写代码:编码时严格遵循 pitfalls.md 中的规则
- 查编码:修改后确认文件编码为 UTF-8 无 BOM
- 重启测试:测试前完全重启 Workshop Tools(清除编译缓存)
- 看日志:在 VConsole 中搜索
ERROR、FATAL、Failed 确认无报错
遇到不确定的问题,先找资料再动手,不要凭猜测修改。
Addon 项目结构
dota 2 beta/
├── game/dota_addons/<addon_name>/ ← "game" 侧 (服务端逻辑+数据)
│ ├── scripts/
│ │ ├── vscripts/ ← Lua 脚本 (VScript)
│ │ │ ├── addon_game_mode.lua ← 必须存在的入口文件
│ │ │ └── ...
│ │ └── npc/ ← KV 数据文件
│ │ ├── herolist.txt ← 可选英雄列表
│ │ ├── npc_heroes_custom.txt ← 英雄覆盖/自定义
│ │ ├── npc_abilities_custom.txt ← 自定义技能
│ │ ├── npc_abilities_override.txt ← 覆盖原版技能数值
│ │ ├── npc_items_custom.txt ← 自定义物品
│ │ └── npc_units_custom.txt ← 自定义单位
│ └── resource/
│ └── addon_<lang>.txt ← 本地化字符串
└── content/dota_addons/<addon_name>/ ← "content" 侧 (UI+资源)
└── panorama/
├── layout/custom_game/ ← XML 布局
├── scripts/custom_game/ ← JS/TS 逻辑
└── styles/custom_game/ ← CSS 样式
addon_game_mode.lua 入口模板
if MyGameMode == nil then
MyGameMode = class({})
end
function Precache(context)
PrecacheResource("soundfile", "soundevents/game_sounds_heroes/game_sounds_sven.vsndevts", context)
PrecacheUnitByNameSync("npc_dota_creature_basic_zombie", context)
end
function Activate()
GameRules.GameMode = MyGameMode()
GameRules.GameMode:InitGameMode()
end
function MyGameMode:InitGameMode()
GameRules:SetCustomGameTeamMaxPlayers(DOTA_TEAM_GOODGUYS, 5)
GameRules:SetCustomGameTeamMaxPlayers(DOTA_TEAM_BADGUYS, 0)
GameRules:SetPreGameTime(30.0)
GameRules:SetCreepSpawningEnabled(false)
GameRules:GetGameModeEntity():SetThink("OnThink", self, "GlobalThink", 2)
ListenToGameEvent("npc_spawned", Dynamic_Wrap(MyGameMode, "OnNPCSpawned"), self)
ListenToGameEvent("entity_killed", Dynamic_Wrap(MyGameMode, "OnEntityKilled"), self)
end
function MyGameMode:OnThink()
if GameRules:State_Get() == DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then
elseif GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then
return nil
end
return 1
end
KV 文件语法
KV 使用 "Key" "Value" 或 "Key" { ... } 嵌套,所有文件必须有正确的根节点。
npc_units_custom.txt — 自定义单位
模型路径必须使用 VPK 中实际存在的编译模型。旧版路径(如 creep_bad_ranged/creep_bad_ranged.vmdl)已被移除,会显示为错误占位体。完整可用模型列表见 models-reference.md。
"DOTAUnits"
{
"Version" "1"
"npc_dota_creature_melee_grunt"
{
"BaseClass" "npc_dota_creature"
"Model" "models/creeps/lane_creeps/creep_bad_melee/creep_bad_melee.vmdl"
"ModelScale" "1.0"
"Level" "1"
"Ability1" ""
"ArmorPhysical" "2"
"MagicalResistance" "0"
"AttackCapabilities" "DOTA_UNIT_CAP_MELEE_ATTACK"
"AttackDamageMin" "25"
"AttackDamageMax" "30"
"AttackRate" "1.6"
"AttackRange" "128"
"MovementCapabilities" "DOTA_UNIT_CAP_MOVE_GROUND"
"MovementSpeed" "300"
"StatusHealth" "500"
"StatusHealthRegen" "0"
"StatusMana" "0"
"TeamName" "DOTA_TEAM_BADGUYS"
"CombatClassAttack" "DOTA_COMBAT_CLASS_ATTACK_BASIC"
"CombatClassDefend" "DOTA_COMBAT_CLASS_DEFEND_WEAK"
"UnitRelationshipClass" "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_DEFAULT"
"VisionDaytimeRange" "800"
"VisionNighttimeRange" "600"
"BountyXP" "30"
"BountyGoldMin" "15"
"BountyGoldMax" "20"
"Creature"
{
"DisableClumpingBehavior" "1"
"DefaultState" "Assault"
"States"
{
"Assault"
{
"Name" "Assault"
"Aggression" "100.0"
"Avoidance" "0.0"
"Support" "0.0"
}
}
}
}
}
npc_heroes_custom.txt — 英雄覆盖
"DOTAHeroes"
{
"npc_dota_hero_sven"
{
"override_hero" "npc_dota_hero_sven"
"Ability1" "sven_storm_hammer"
"Ability2" "sven_great_cleave"
"Ability3" "sven_warcry"
"Ability6" "sven_gods_strength"
}
}
npc_abilities_override.txt — 覆盖原版技能数值
"DOTAAbilities"
{
"Version" "1"
"sven_storm_hammer"
{
"AbilityCooldown" "8.0 7.0 6.0 5.0"
"AbilityDamage" "150 225 300 375"
}
}
herolist.txt — 限定可选英雄
"CustomHeroList"
{
"npc_dota_hero_sven" "1"
"npc_dota_hero_lina" "1"
"npc_dota_hero_axe" "1"
"npc_dota_hero_dazzle" "1"
"npc_dota_hero_sniper" "1"
}
Lua API 核心函数速查
单位操作
local unit = CreateUnitByName("npc_name", position, true, nil, nil, DOTA_TEAM_BADGUYS)
ExecuteOrderFromTable({
UnitIndex = unit:GetEntityIndex(),
OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
Position = targetPos
})
local units = FindUnitsInRadius(
teamNumber, position, nil, radius,
DOTA_UNIT_TARGET_TEAM_ENEMY,
DOTA_UNIT_TARGET_HERO + DOTA_UNIT_TARGET_BASIC,
DOTA_UNIT_TARGET_FLAG_NONE,
FIND_ANY_ORDER, false
)
ApplyDamage({
victim = target,
attacker = caster,
damage = 200,
damage_type = DAMAGE_TYPE_MAGICAL
})
target:AddNewModifier(caster, ability, "modifier_name", {duration = 5})
GameRules 常用设置
GameRules:SetCustomGameTeamMaxPlayers(DOTA_TEAM_GOODGUYS, 5)
GameRules:SetCustomGameTeamMaxPlayers(DOTA_TEAM_BADGUYS, 0)
GameRules:SetPreGameTime(30.0)
GameRules:SetCreepSpawningEnabled(false)
GameRules:SetTreeRegrowTime(60.0)
GameRules:SetHeroSelectionTime(30.0)
GameRules:SetGoldPerTick(0)
GameRules:SetGoldTickTime(0)
GameRules:SetUseUniversalShopMode(true)
GameRules:GetGameModeEntity():SetCustomHeroMaxLevel(25)
GameRules:GetGameModeEntity():SetFogOfWarDisabled(true)
Timers (需引入 timers.lua 库)
Timers:CreateTimer(5.0, function()
end)
Timers:CreateTimer(function()
return 1.0
end)
事件监听
ListenToGameEvent("npc_spawned", function(event)
local unit = EntIndexToHScript(event.entindex)
end, nil)
ListenToGameEvent("entity_killed", function(event)
local killed = EntIndexToHScript(event.entindex_killed)
local killer = EntIndexToHScript(event.entindex_attacker)
end, nil)
ListenToGameEvent("dota_player_pick_hero", function(event)
local hero = EntIndexToHScript(event.heroindex)
local playerID = event.player
end, nil)
Lua 自定义技能 (CDOTA_Ability_Lua)
KV 注册 (npc_abilities_custom.txt)
"my_custom_ability"
{
"BaseClass" "ability_lua"
"ScriptFile" "abilities/my_custom_ability"
"AbilityTextureName" "sven_storm_hammer"
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_UNIT_TARGET"
"AbilityUnitTargetTeam" "DOTA_UNIT_TARGET_TEAM_ENEMY"
"AbilityUnitTargetType" "DOTA_UNIT_TARGET_HERO | DOTA_UNIT_TARGET_BASIC"
"AbilityUnitDamageType" "DAMAGE_TYPE_MAGICAL"
"AbilityCastRange" "600"
"AbilityCooldown" "10.0 9.0 8.0 7.0"
"AbilityManaCost" "100 120 140 160"
"AbilityValues"
{
"damage" "100 175 250 325"
"duration" "2.0"
}
}
Lua 技能脚本 (vscripts/abilities/my_custom_ability.lua)
my_custom_ability = class({})
function my_custom_ability:OnSpellStart()
local caster = self:GetCaster()
local target = self:GetCursorTarget()
local damage = self:GetSpecialValueFor("damage")
local duration = self:GetSpecialValueFor("duration")
ApplyDamage({
victim = target, attacker = caster,
damage = damage, damage_type = self:GetAbilityDamageType(),
ability = self
})
target:AddNewModifier(caster, self, "modifier_my_stun", {duration = duration})
EmitSoundOn("Hero_Sven.StormBolt.Target", target)
end
Lua Modifier (vscripts/abilities/modifier_my_stun.lua)
modifier_my_stun = class({})
LinkLuaModifier("modifier_my_stun", "abilities/modifier_my_stun", LUA_MODIFIER_MOTION_NONE)
function modifier_my_stun:IsStunDebuff() return true end
function modifier_my_stun:IsDebuff() return true end
function modifier_my_stun:CheckState()
return { [MODIFIER_STATE_STUNNED] = true }
end
function modifier_my_stun:GetEffectName()
return "particles/generic_gameplay/generic_stunned.vpcf"
end
function modifier_my_stun:GetEffectAttachType()
return PATTACH_OVERHEAD_FOLLOW
end
Panorama UI (HUD)
XML 布局 (content/panorama/layout/custom_game/hud.xml)
<root>
<styles>
<include src="file://{resources}/styles/custom_game/hud.css" />
</styles>
<scripts>
<include src="file://{resources}/scripts/custom_game/hud.js" />
</scripts>
<Panel class="RootOuter" hittest="false">
<Panel id="CustomHUD">
<Panel id="WaveInfo">
<Label id="WaveText" text="Wave: 1/10" />
<Label id="EnemyCount" text="Enemies: 0" />
</Panel>
<Panel id="AncientHP">
<Panel id="AncientHPBar" />
<Label id="AncientHPText" text="5000/5000" />
</Panel>
</Panel>
</Panel>
</root>
JS 逻辑 (content/panorama/scripts/custom_game/hud.js)
function UpdateHUD() {
var data = CustomNetTables.GetTableValue("game_state", "wave_info");
if (data) {
$("#WaveText").text = "Wave: " + data.current_wave + "/" + data.max_waves;
$("#EnemyCount").text = "Enemies: " + data.enemies_alive;
}
$.Schedule(0.1, UpdateHUD);
}
CustomNetTables.SubscribeNetTableListener("game_state", function(table, key, data) {
if (key === "wave_info") {
$("#WaveText").text = "Wave: " + data.current_wave + "/" + data.max_waves;
}
});
UpdateHUD();
CSS 样式 (content/panorama/styles/custom_game/hud.css)
#CustomHUD {
width: 100%; height: 100%;
}
#WaveInfo {
horizontal-align: center; margin-top: 10px;
flow-children: right;
}
#WaveInfo Label {
color: white; font-size: 24px;
margin-right: 20px; text-shadow: 2px 2px 4px black;
}
服务端↔客户端通信
CustomNetTables (持久化同步)
CustomNetTables:SetTableValue("game_state", "wave_info", {
current_wave = 2,
max_waves = 10,
enemies_alive = 15
})
var data = CustomNetTables.GetTableValue("game_state", "wave_info");
CustomNetTables.SubscribeNetTableListener("game_state", function(table, key, data) {});
CustomGameEvents (一次性事件)
CustomGameEventManager:Send_ServerToAllClients("boss_spawned", { boss_name = "Roshan" })
GameEvents.Subscribe("boss_spawned", function(data) {
$.Msg("Boss: " + data.boss_name);
});
GameEvents.SendCustomGameEventToServer("player_ready", {});
Hammer 地图关键实体
| 实体类型 | 用途 | 关键属性 |
|---|
info_target | 命名位置点 (刷兵staging/路径点) | targetname |
path_corner | 路径航点 (兵线路径) | targetname, target (下一个点) |
npc_dota_spawner | 小兵刷新器 | Name, First Waypoint |
trigger_shop | 商店触发区域 | shoptype (0=home/1=side/2=secret) |
ent_dota_tree | 树木 | — |
npc_dota_building | 建筑 (塔/兵营/Ancient) | 模型路径 |
info_player_start_goodguys | 天辉出生点 | — |
info_player_start_badguys | 夜魇出生点 | — |
开发与调试
-- 控制台启动自定义游戏
dota_launch_custom_game <addon_name> <map_name>
-- 重启 (重载 Lua 脚本)
restart
-- 常用调试命令
dota_create_unit npc_dota_creature_melee_grunt
dota_bot_populate -- 添加 Bot
dota_dev hero_refresh -- 刷新英雄技能/血量
-gold 99999 -- 给予金币
-lvlup 25 -- 升级
模型选择指南
选择 creature/单位模型时,必须使用 VPK 中实际存在的编译模型路径。
三大模型来源
| 来源 | 前缀 | 用途 |
|---|
| Creep 小兵/中立生物 | models/creeps/ | 各种怪物、小兵、Boss |
| 英雄基础模型 | models/heroes/<hero>/ | 可直接用于 creature Model |
| 英雄饰品 | models/items/<hero>/ | 搭配英雄模型做外观变体 |
查询工具
python scripts/query_models.py <vpk_path> --hero sven
python scripts/query_models.py <vpk_path> --search dragon
python scripts/query_models.py <vpk_path> --type creeps
敌人变体设计
同一英雄模型 + 不同饰品 = 不同等级/类型的敌人:
- 普通: 默认英雄模型
- 精英: + 饰品 Wearable (通过 Lua 动态挂载)
- Boss: + 全套饰品 + ModelScale 放大
详细参考文档