- 游戏开发
【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
导读
在 VCMI 的 Lua 脚本 API 中,全局表ENUM是 mod 开发者与引擎底层枚举之间的"翻译层":它以字符串键暴露了一批整数常量,避免在脚本里书写难以维护的魔法数字。本文以官方参考文档 Enums.md 为核心,完整整理ENUM下全部 15 个枚举分组的键值对照表,并结合引擎源码(luascript/api/Enums.cpp)与仓库内真实脚本(scripts)说明每一组枚举的语义、适用场景与实战写法。读完本文,你将能熟练使用ENUM.HealLevel、ENUM.BonusDuration、ENUM.WallPart等常量编写战斗脚本、护盾/治疗效果与攻城逻辑。
一、ENUM 是什么:从魔法数字到具名常量
VCMI 允许 mod 通过 Lua 脚本(位于 scripts 目录,含 combat、spells、damage 三个子目录)扩展战斗、魔法与地图行为。引擎内部的枚举(如治疗等级、施法错误码、奖励来源)在 C++ 中是以整数表达的,如果 Lua 侧直接书写0、2、7这类数字,代码将难以阅读和维护。
ENUM正是为解决这一问题而注册的全局表。其官方定位为:
全局
ENUM表 —— 脚本用于避免魔法数字的 string→integer 常量。表中的每个字段都是一个具名分组;运行时其值是一个 Lua 表,键为常量名。
也就是说,ENUM的结构是两层嵌套表:
ENUM.<分组名>.<常量名> -- 例如 ENUM.HealLevel.resurrect == 1该全局表的注册位置在 luascript/LuaContext.cpp:
api::Enums enums; S.push(env->game()); lua_setglobal(L, "GAME"); S.push(env->services()); lua_setglobal(L, "LIBRARY"); S.push(enums); lua_setglobal(L, "ENUM");引擎在初始化 Lua 上下文时依次注册了GAME、LIBRARY、ENUM三个全局对象,其中ENUM由api::Enums序列化而来。
从源码看分组如何导出
Enums类的声明位于 luascript/api/Enums.h,它通过serializeScript逐个注册分组,例如:
s("HealLevel", exportHealLevel(), "Outcome categories for the heal action: plain heal / resurrect / overheal."); s("BonusDuration", exportBonusDuration(), "Lifetime selectors used by Bonus / BonusDescriptor `duration`."); s("WallPart", exportWallPart(), "Town-wall sections referenced by siege APIs and `catapultAttack`.");每个分组的实际内容定义在 luascript/api/Enums.cpp 中,其结构是EnumItem(键名 + C++ 枚举值 + 悬停描述)的列表,最终由模板EnumGroup承载。脚本读取ENUM.<Group>.<key>时得到的就是对应的 C++ 枚举值本身。
这套结构同时服务于 LuaLS(Lua Language Server)的类型提示:文档生成器 luascript/api/DocExport.cpp 会为每个枚举分组生成---@enum注解,使编辑器能自动补全ENUM.HealLevel.heal并在悬停时显示描述。
二、治疗与复活:HealLevel 与 HealPower
HealLevel —— 治疗动作的结果类别
治疗动作的三种结果类别:普通治疗 / 复活 / 过量治疗。对应的 C++ 定义是EHealLevel(lib/constants/Enumerations.h)。
| Key | Value | 说明 |
|---|---|---|
heal | 0 | 标准治疗——上限为当前部队数量,不复活阵亡单位 |
resurrect | 1 | 复活部队中阵亡的单位,最高恢复至开战时数量 |
overheal | 2 | 允许部队数量超过开战时数量(过量治疗) |
导出实现见 luascript/api/Enums.cpp。
HealPower —— 治疗效果的存续性
治疗(尤其复活)效果在战斗结束后是否保留:仅本场战斗 vs 永久保留。C++ 定义是EHealPower(lib/constants/Enumerations.h)。
| Key | Value | 说明 |
|---|---|---|
oneBattle | 0 | 被复活的部队在战斗结束后消失 |
permanent | 1 | 被复活的部队保留到战斗后的部队状态 |
导出实现见 luascript/api/Enums.cpp。
实战:lifeDrain 与重生脚本中的组合使用
HealLevel与HealPower通常成对传给server:healUnit。仓库内的 scripts/combat/lifeDrain.lua 演示了"复活 + 永久保留"的写法:
local healed, resurrected = server:healUnit(battle, unit, toHeal, ENUM.HealLevel.resurrect, ENUM.HealPower.permanent) if healed <= 0 then return end而 scripts/combat/rebirth.lua 在重生效果中也使用了同样的组合。若希望复活仅作用于本场战斗(例如某些一次性法术),则应改用ENUM.HealPower.oneBattle。
三、施法校验:SpellCastProblem 错误码
SpellCastProblem是施法合法性校验返回的错误码集合,脚本可用它判断施法失败原因并给出相应反馈。C++ 定义是ESpellCastProblem(lib/constants/Enumerations.h),注意该枚举还有一个OK = 0的隐式成功值(源码中列为OK,位于 NO_HERO_TO_CAST_SPELL 之前),因此所有错误码从 1 开始。
| Key | Value | 说明 |
|---|---|---|
noHeroToCastSpell | 1 | 没有可用英雄来施放该法术 |
castsPerTurnLimit | 2 | 施法者已用完本回合的施法次数 |
noSpellbook | 3 | 施法者没有携带魔法书 |
heroDoesntKnowSpell | 4 | 施法者的魔法书中没有该法术 |
notEnoughMana | 5 | 施法者没有足够法力施放该法术 |
advmapSpellInsteadOfBattleSpell | 6 | 在战斗上下文中尝试施放了冒险地图法术 |
spellLevelLimitExceeded | 7 | 施法者的智慧术等级不足以施放该等级法术 |
noSpellsToDispel | 8 | 施放了驱散但没有可移除的法术 |
noAppropriateTarget | 9 | 未为法术选择合法目标 |
stackImmuneToSpell | 10 | 所选目标单位对该法术免疫 |
wrongSpellTarget | 11 | 所选目标不匹配该法术的 AimType |
ongoinTacticPhase | 12 | 战术部署阶段不允许施法 |
magicIsBlocked | 13 | 战场上魔法被抑制(如禁魔地形或类似魔法抑制之球的效果) |
导出实现见 luascript/api/Enums.cpp。注意源码中ONGOING_TACTIC_PHASE的拼写,与 Lua 侧键名ongoinTacticPhase(少一个 g)保持了一致,使用时请按表中键名书写。
实战:catapult 脚本中的校验错误码
scripts/spells/catapult.lua 在攻城法术的通用校验中使用ENUM.SpellCastProblem.noAppropriateTarget报告"无合法目标":
function Script:applicableGeneral(mechanics, problem) local battle = mechanics:getBattle() if not battle:hasFortifications() or (mechanics:isSmart() and mechanics:getCasterSide() ~= ENUM.BattleSide.attacker) or #potentialTargets(battle) == 0 then problem:addStandard(mechanics, ENUM.SpellCastProblem.noAppropriateTarget) return false end ... end四、法术目标模式:AimType
AimType描述一个法术要求的目标类型:无目标、生物、障碍物、位置。C++ 定义位于 include/vcmi/spells/Magic.h:
enum class AimType { NOTHING, CREATURE, OBSTACLE, LOCATION };| Key | Value | 说明 |
|---|---|---|
nothing | 0 | 法术不需要目标——选择后直接施放的范围法术 |
creature | 1 | 法术以战场上的一个或多个单位为对象 |
obstacle | 2 | 法术以已存在的障碍物为对象 |
location | 3 | 法术以一个或多个战场格子为对象(无需单位) |
导出实现见 luascript/api/Enums.cpp。在引擎内部,lib/battle/CBattleInfoCallback.cpp 依据AimType过滤候选目标:CREATURE收集单位、OBSTACLE收集障碍、LOCATION收集格子;当目标类型列表以NOTHING开头时则直接跳过目标筛选。
五、奖励系统四大枚举:BonusDuration / BonusSource / BonusValueType / BonusLimitEffect
VCMI 的奖励(Bonus)系统是战斗与成长数值的核心,ENUM中的四个分组正好对应 Bonus 与 BonusDescriptor 的四个关键字段。它们的 C++ 定义集中在 lib/bonuses/BonusEnum.h。
BonusDuration —— 奖励的存续期(duration 字段)
定义见 lib/bonuses/BonusEnum.h。与其它枚举不同,它使用uint16_t位掩码(每个值是 2 的幂),因此同一奖励可同时组合多个存续条件(例如同时"一回合"与"直到被攻击")。
| Key | Value | 说明 |
|---|---|---|
permanent | 1 | 永久生效(直到被显式移除) |
oneBattle | 2 | 当前战斗结束时过期 |
oneDay | 4 | 当前游戏日结束时过期 |
oneWeek | 8 | 当前游戏周结束时过期(注意:持续到本周结束,不是整整 7 天) |
nTurns | 16 | N 个战斗回合后过期(战斗期间使用,战斗结束后奖励总是被移除) |
nDays | 32 | N 个游戏日后过期 |
untilBeingAttacked | 64 | 持有者第一次受到攻击时过期(每次单位受伤都会触发) |
untilAttack | 128 | 持有者发起任意攻击(含反击)时过期 |
stackGetsTurn | 256 | 持有者所属部队行动时过期(用于防御姿态) |
commanderKilled | 512 | 与指挥官绑定——指挥官死亡时过期 |
untilOwnAttack | 1024 | 持有者主动发起攻击(非反击)时过期 |
untilTakingIndirectDamage | 2048 | 持有者受到间接伤害时过期(法术或环境伤害,不含近战/远程生物攻击、箭塔或弩车伤害) |
untilAfterAttackSequence | 4096 | 当前攻击与反击序列(含生物攻击、箭塔与战争机器)结算后过期 |
导出实现见 luascript/api/Enums.cpp。
BonusSource —— 奖励的来源类别(sourceType 字段)
定义见 lib/bonuses/BonusEnum.h 的BONUS_SOURCE_LIST宏与 lib/bonuses/BonusEnum.h 的枚举声明。它用于标记奖励来自哪一类系统,某些来源间存在叠加/去重规则。
| Key | Value | 说明 |
|---|---|---|
artifact | 0 | 由宝物定义授予;英雄携带多个相同宝物时不叠加 |
artifactInstance | 1 | 由具体装备的宝物实例授予;多件相同宝物可叠加 |
objectType | 2 | 由地图对象类型授予(如马厩) |
objectInstance | 3 | 由地图对象的特定实例授予;每个相同对象分别叠加 |
creatureAbility | 4 | 生物类型固有(如龙息、亡灵) |
terrainNative | 5 | 主场地形奖励(生物位于其阵营的家乡地形) |
terrainOverlay | 6 | 战场地形覆盖奖励(魔法平原、火径等) |
spellEffect | 7 | 持有者身上的法术效果 |
townStructure | 8 | 已建造的城镇建筑 |
heroBaseSkill | 9 | 英雄主技能(攻击/防御/力量/知识) |
secondarySkill | 10 | 英雄辅助技能等级(战术、进攻术、智慧术等) |
heroSpecial | 11 | 英雄特长(生物亲和、法术亲和等) |
army | 12 | 部队构成奖励;目前仅用于士气 |
campaignBonus | 13 | 战役场景带来的继承奖励 |
stackExperience | 14 | 部队经验等级奖励 |
commander | 15 | 指挥官单位能力 |
global | 16 | 地图范围的全局效果(在游戏设置中定义) |
other | 17 | 无法归入其它类别的来源 |
导出实现见 luascript/api/Enums.cpp。
BonusValueType —— 奖励数值的组合规则(valueType 字段)
定义见 lib/bonuses/BonusEnum.h,它决定奖励值在叠加计算中以何种方式参与。
| Key | Value | 说明 |
|---|---|---|
additiveValue | 0 | 加到基础值上,但在percentToBase之后计算 |
baseNumber | 1 | 在任何加法/百分比步骤之前,先加入基础值 |
percentToAll | 2 | 百分比作用于所有来源合计的累计值 |
percentToBase | 3 | 百分比只作用于基础值 |
percentToSource | 4 | 百分比作用于同来源小计 |
percentToTargetType | 5 | 百分比只作用于匹配 targetSourceType 的奖励 |
independentMax | 6 | 独立上限——若大于累计值则取该值 |
independentMin | 7 | 独立下限——若小于累计值则取该值 |
导出实现见 luascript/api/Enums.cpp。
BonusLimitEffect —— 奖励的战斗适用范围(effectRange 字段)
定义见 lib/bonuses/BonusEnum.h。源码注释特别指出:它用于标记如"精确"(Precision)这类只影响远程攻击的攻击/防御主技能奖励。
| Key | Value | 说明 |
|---|---|---|
noLimit | 0 | 适用于所有战斗类型 |
onlyDistanceFight | 1 | 仅射击(远程)时生效 |
onlyMeleeFight | 2 | 仅近战时生效 |
导出实现见 luascript/api/Enums.cpp。
实战:三个仓库内脚本的 Bonus 用法
- scripts/combat/ballistaDamage.lua:查询来自宝物的攻击加成时,把
ENUM.BonusSource.artifact与heroBaseSkill分别作为sourceType过滤条件,将"宝物攻击"与"英雄基础攻击"分离统计。 - scripts/combat/rebirth.lua:为部队添加"免疫反击"奖励时组合使用
ENUM.BonusDuration.nTurns(配合turns = 1)与ENUM.BonusSource.creatureAbility。 - scripts/combat/soulSteal.lua:使用
ENUM.BonusDuration.oneBattle标记本场战斗内有效的标记性奖励。
server:addUnitBonus(battle, unit, { type = "NO_RETALIATION", val = 0, duration = ENUM.BonusDuration.nTurns, turns = 1, sourceType = ENUM.BonusSource.creatureAbility, sourceID = unit:getCreature():getJsonKey() }, false)由于BonusDuration是位掩码,若需要"战斗内且直到被攻击前"这类复合条件,可以用ENUM.BonusDuration.oneBattle + ENUM.BonusDuration.untilBeingAttacked(或位或运算)组合。
六、战场元素:ObstacleType、WallPart 与 BattleSide
ObstacleType —— 障碍物类别
用于 SpellObstacleDescriptor 的obstacleType字段。C++ 定义在 lib/battle/CObstacleInstance.h:
enum EObstacleType : ui8 { USUAL, ABSOLUTE_OBSTACLE, SPELL_CREATED, MOAT };| Key | Value | 说明 |
|---|---|---|
usual | 0 | 战斗开始时随机放置的小型战场障碍物 |
absolute | 1 | 阻挡战场固定区域的大型障碍物;地图上最多同时存在一个 |
spellCreated | 2 | 由法术生成的障碍物(地雷、流沙、火墙等) |
moat | 3 | 城镇攻防战中的护城河地块 |
导出实现见 luascript/api/Enums.cpp。实战示例见 scripts/spells/moat.lua:根据法术是否可驱散,选择ENUM.ObstacleType.spellCreated或ENUM.ObstacleType.moat作为障碍类型。
WallPart —— 城镇城墙区段
攻城 API 与catapultAttack引用的城墙区段标识。C++ 定义在 lib/constants/Enumerations.h,前三个是负数哨兵值,从 0 开始才是真实区段:
enum class EWallPart : int8_t { INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, PARTS_COUNT };| Key | Value | 说明 |
|---|---|---|
invalid | -1 | 无城墙区段;用于城防范围之外的格子 |
indestructiblePart | -2 | 始终存在且始终阻挡远程攻击的城墙段 |
indestructibleGate | -3 | 始终存在但不阻挡远程攻击的城门段 |
keep | 0 | 中央城堡,通常由要塞(Citadel)建造 |
bottomTower | 1 | 城墙底部箭塔,通常由城堡(Castle)建造 |
bottomWall | 2 | 底部箭塔旁可摧毁的城墙段 |
belowGate | 3 | 城门正下方的可摧毁城墙段 |
overGate | 4 | 城门正上方的可摧毁城墙段 |
upperWall | 5 | 上部箭塔旁的可摧毁城墙段 |
upperTower | 6 | 城墙顶部箭塔,通常由城堡(Castle)建造 |
gate | 7 | 可摧毁的城门,只有防守方可以开启 |
导出实现见 luascript/api/Enums.cpp。
实战:catapult 攻城脚本(scripts/spells/catapult.lua)把城墙区段组织为常量表,并映射每个区段的命中概率:
local WALLS = { ENUM.WallPart.upperWall, ENUM.WallPart.overGate, ENUM.WallPart.belowGate, ENUM.WallPart.bottomWall } local TOWERS = { ENUM.WallPart.upperTower, ENUM.WallPart.bottomTower } local KEEP = ENUM.WallPart.keep local GATE = ENUM.WallPart.gateBattleSide —— 战场阵营标识
战场双方的阵营标识,同时提供"攻击方/防守方"与"左侧/右侧"两套等价命名。C++ 定义在 lib/battle/BattleSide.h,其中ATTACKER == LEFT_SIDE、DEFENDER == RIGHT_SIDE:
| Key | Value | 说明 |
|---|---|---|
none | -1 | 与任何一方无关 |
attacker | 0 | 发起战斗的攻击方军队;等价于left |
defender | 1 | 防守方军队;等价于right |
left | 0 | 从战场左侧开局的军队;等价于attacker |
right | 1 | 从战场右侧开局的军队;等价于defender |
导出实现见 luascript/api/Enums.cpp。实战示例: scripts/combat/summonGuardians.lua 用side == ENUM.BattleSide.attacker判断召唤单位属于攻击方,进而执行 H3 原版"不可在敌方边界附近生成单位"的摆放规则;scripts/spells/catapult.lua 则用mechanics:getCasterSide() ~= ENUM.BattleSide.attacker限制智能施法只允许攻击方使用。
七、游戏进度:Difficulty、PlayerColor、PlayerStatus
Difficulty —— 游戏难度
五个难度等级以国际象棋棋子命名(兵最易、王最难),括号内为该难度的 AI 经济/资源系数。C++ 定义在 lib/mapping/MapDifficulty.h:
enum class EMapDifficulty : uint8_t { EASY = 0, NORMAL = 1, HARD = 2, EXPERT = 3, IMPOSSIBLE = 4, COUNT = 5 };| Key | Value | 说明 |
|---|---|---|
pawn | 0 | 简单难度(80%) |
knight | 1 | 普通难度(100%) |
rook | 2 | 困难难度(130%) |
queen | 3 | 专家难度(160%) |
king | 4 | 极难难度(200%) |
导出实现见 luascript/api/Enums.cpp。
PlayerColor —— 玩家颜色与中立归属
八种玩家颜色,外加neutral表示无主对象的归属(未占领城镇、矿场、巢穴等)。C++ 侧PlayerColor是基于StaticIdentifier的类,其哨兵值在 lib/constants/EntityIdentifiers.h 中定义(NEUTRAL = 255等),而 Lua 侧导出则遍历PlayerColor::ALL_PLAYERS()并追加neutral(见 luascript/api/Enums.cpp)。
| Key | Value | 说明 |
|---|---|---|
red | 0 | 红色玩家 |
blue | 1 | 蓝色玩家 |
tan | 2 | 褐色玩家 |
green | 3 | 绿色玩家 |
orange | 4 | 橙色玩家 |
purple | 5 | 紫色玩家 |
teal | 6 | 青色玩家 |
pink | 7 | 粉色玩家 |
neutral | -1 | 无归属——用于未被占领的城镇、矿场和巢穴 |
PlayerStatus —— 玩家游戏状态
玩家是否仍在游戏中、已获胜或已失败。C++ 定义在 lib/constants/Enumerations.h(WRONG = -1为内部哨兵值,未导出)。
| Key | Value | 说明 |
|---|---|---|
ingame | 0 | 玩家仍在游戏中 |
loser | 1 | 玩家已被击败并退出游戏 |
winner | 2 | 玩家已赢得游戏 |
导出实现见 luascript/api/Enums.cpp。
八、英雄成长:PrimarySkill
四大英雄主技能,常与 HeroInstance 相关 API 配合使用。C++ 定义在 lib/constants/EntityIdentifiers.h(PrimarySkill::ATTACK、DEFENSE、SPELL_POWER、KNOWLEDGE)。
| Key | Value | 说明 |
|---|---|---|
attack | 0 | 攻击技能,提高英雄部队造成的伤害 |
defence | 1 | 防御技能,降低英雄部队受到的伤害 |
spellpower | 2 | 法术力量,放大英雄法术的效果 |
knowledge | 3 | 知识,提高英雄的最大法力值 |
导出实现见 luascript/api/Enums.cpp。该枚举经常作为"子类型"与BonusSource.heroBaseSkill搭配出现,例如 scripts/combat/ballistaDamage.lua 中{ type = "PRIMARY_SKILL", subtype = "attack", ... }的subtype = "attack"即对应ENUM.PrimarySkill.attack的语义。
九、分组速查与使用建议
全部 15 个分组的官方文档索引
| 分组 | 用途 | 官方子文档 |
|---|---|---|
| HealLevel | 治疗结果类别(治疗/复活/过量) | HealLevel.md |
| HealPower | 治疗效果存续性(仅本场/永久) | HealPower.md |
| SpellCastProblem | 施法校验错误码 | SpellCastProblem.md |
| AimType | 法术目标模式 | AimType.md |
| BonusDuration | 奖励存续期(位掩码) | BonusDuration.md |
| BonusSource | 奖励来源类别 | BonusSource.md |
| BonusValueType | 奖励数值组合规则 | BonusValueType.md |
| BonusLimitEffect | 奖励的战斗适用范围 | BonusLimitEffect.md |
| ObstacleType | 障碍物类别 | ObstacleType.md |
| WallPart | 城墙区段 | WallPart.md |
| BattleSide | 战场阵营 | BattleSide.md |
| Difficulty | 游戏难度 | Difficulty.md |
| PrimarySkill | 四大主技能 | PrimarySkill.md |
| PlayerColor | 玩家颜色与中立归属 | PlayerColor.md |
| PlayerStatus | 玩家游戏状态 | PlayerStatus.md |
使用要点
- 无需记忆数字:所有场景一律通过
ENUM.<Group>.<key>引用常量;数字只是文档对照值,脚本中不应硬编码。 - 注意位掩码分组:
BonusDuration的值是 2 的幂,可组合;其余分组大多为互斥的普通枚举。 - 拼写以文档为准:个别键名沿用了引擎源码的拼写(如
ongoinTacticPhase),请以本文表格与官方子文档为准。 - 与 Bonus API 联动:
BonusDuration、BonusSource、BonusValueType、BonusLimitEffect四组分别对应 BonusDescriptor 的duration、sourceType、valueType、effectRange字段,构造奖励表时直接引用即可获得编辑器补全与悬停说明。 - 类型提示支持:
ENUM表由 DocExport.cpp 生成的注解驱动,支持 LuaLS 自动补全与悬停文档,建议在带 LuaLS 的编辑器中编写 mod 脚本。
延伸阅读:完整的全局 API 概览见 API.md;奖励结构见 Bonus.md 与 BonusDescriptor.md;战斗接口见 Battle.md 与 BattleServer.md。
- 游戏开发
【免费下载链接】vcmi
Open-source engine for Heroes of Might and Magic III
相关推荐
TypeScript 枚举(Enum)完全指南:数字枚举、字符串枚举与常量枚举实战详解
TypeScript 枚举(Enum)完全指南:数字枚举、字符串枚举与常量枚举实战详解 本指南基于《TypeScript Deep Dive》中文版(types
文档教程VCMI 模组 Lua API 参考:BonusSource 奖励来源枚举详解
VCMI 模组 Lua API 参考:BonusSource 奖励来源枚举详解 导读 BonusSource (奖励来源)是 VCMI 奖励(Bonus)系统中
游戏开发PostGraphile 枚举(Enum)完整指南:PostgreSQL 枚举、Enum 表与 extendSchema 的三种实践方案
PostGraphile 枚举(Enum)完整指南:PostgreSQL 枚举、Enum 表与 extendSchema 的三种实践方案 导读 本指南聚焦 Po
后端API网关
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考