伤害系统
EmakiAttribute 的伤害计算是阶段式的:一次攻击会经过多个计算阶段(基础伤害 → 暴击 → 防御减免),每个阶段的输出作为下一阶段的输入,最终得到实际伤害值。伤害类型、计算阶段、公式全部通过 YAML 配置,不需要改代码就能调整。
伤害类型定义
伤害类型文件放在 damage_types/ 目录下,每个 YAML 文件定义一个伤害类型。
YAML 格式
id: physical
display_name: "物理伤害"
aliases:
- "melee"
- "physical_damage"
allowed_events:
- entity_attack
- entity_sweep_attack
hard_lock: false
stages:
- id: base_attack
kind: FLAT_PERCENT
source: ATTACKER
mode: ADD
flat_attributes:
- physical_attack
percent_attributes:
- physical_damage_bonus
- id: crit
kind: FLAT_PERCENT
source: ATTACKER
mode: ADD
chance_attributes:
- physical_crit_rate
flat_attributes:
- physical_crit_damage
multiplier_attributes: []
min_chance: 0.0
max_chance: 100.0
- id: target_defense
kind: FLAT_PERCENT
source: TARGET
mode: SUBTRACT
flat_attributes:
- physical_defense
percent_attributes: []
recovery:
source: ATTACKER
resistance_source: TARGET
flat_attributes:
- lifesteal
percent_attributes:
- percentage_lifesteal
resistance_attributes:
- lifesteal_resistance
min_result: 0.0字段说明
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
id | String | 是 | — | 伤害类型唯一标识 |
display_name | String | 否 | 同 id | 显示名称 |
aliases | List | 否 | [] | 别名列表,在 MythicMobs 等地方可以用别名引用 |
allowed_events | List | 否 | [] | 允许触发此伤害类型的 Bukkit DamageCause |
hard_lock | boolean | 否 | false | 硬锁定到指定事件。开启后,只有 allowed_events 中的事件才能触发这个伤害类型 |
stages | List | 是 | [] | 计算阶段列表,按顺序执行 |
recovery | Object | 否 | null | 吸血/回复配置 |
description | String | 否 | "" | 描述文本 |
attacker_message | String | 否 | null | 攻击者看到的伤害消息(MiniMessage 格式) |
target_message | String | 否 | null | 受击者看到的伤害消息(MiniMessage 格式) |
内置伤害类型
插件默认带了三个伤害类型,覆盖了最常见的战斗场景:
| ID | 显示名称 | 主要事件 | 说明 |
|---|---|---|---|
physical | 物理伤害 | entity_attack、entity_sweep_attack | 近战物理伤害,玩家挥剑砍怪时触发 |
projectile | 射击伤害 | projectile | 弓箭、弩等投射物命中时触发 |
spell | 法术伤害 | — | 没有绑定原版事件,通常由 MythicMobs 的 emaki_damage 机制触发 |
阶段(Stage)定义
每个伤害类型包含一个有序的阶段列表。伤害值从第一个阶段开始,逐步经过每个阶段的计算,最终输出结果。
阶段字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id | String | "stage" | 阶段标识,某些特殊 ID 有额外行为(见下方) |
kind | Enum | FLAT_PERCENT | 阶段类型 |
source | Enum | ATTACKER | 属性从谁身上读:ATTACKER(攻击者)、TARGET(目标)、CONTEXT(上下文) |
mode | Enum | ADD | 运算模式:ADD(加)或 SUBTRACT(减) |
flat_attributes | List | [] | 固定值属性列表 |
percent_attributes | List | [] | 百分比属性列表 |
chance_attributes | List | [] | 概率属性列表(用于暴击判定等) |
multiplier_attributes | List | [] | 乘数属性列表 |
expression | String | "" | 自定义表达式(仅 CUSTOM 类型使用) |
min_result | Double | null | 阶段结果下限 |
max_result | Double | null | 阶段结果上限 |
min_chance | Double | null | 概率下限 |
max_chance | Double | null | 概率上限 |
min_multiplier | Double | null | 乘数下限 |
max_multiplier | Double | null | 乘数上限 |
FLAT_PERCENT 类型
最常用的阶段类型,计算逻辑是"固定值 + 百分比加成"。
当 mode: ADD 时:
阶段结果 = flat + input × (percent / 100)
最终值 = input + 阶段结果当 mode: SUBTRACT 时:
阶段结果 = flat + input × (percent / 100)
最终值 = input - 阶段结果这里 flat 是 flat_attributes 所有属性值的合计,percent 是 percent_attributes 的合计,input 是上一阶段的输出。
举个例子:攻击者有 100 物理攻击、20% 物理伤害加成,那 base_attack 阶段的结果就是 100 + 0 × (20 / 100) = 100(因为初始 input 是 0),最终值 = 0 + 100 = 100。等等——这里 input 初始值是事件的 baseDamage,通常是原版伤害值或 MythicMobs 传入的基础值。
CUSTOM 类型
如果 FLAT_PERCENT 满足不了你的需求,可以用 CUSTOM 类型写自定义表达式。表达式中可以用这些变量:
| 变量 | 说明 |
|---|---|
{input} | 当前输入值(上一阶段的输出) |
{flat} | flat_attributes 合计值 |
{percent} | percent_attributes 合计值 |
{chance} | chance_attributes 合计值 |
{multiplier} | multiplier_attributes 合计值 |
{crit} | 是否暴击(1 或 0) |
{roll} | 暴击随机数(0–100) |
# 自定义阶段示例
- id: custom_stage
kind: CUSTOM
source: ATTACKER
expression: "{input} * (1 + {percent} / 100) + {flat} * {crit}"特殊阶段 ID
有两个阶段 ID 会触发额外的内置逻辑:
| 阶段 ID | 说明 |
|---|---|
crit / critical | 暴击阶段。系统会根据 chance_attributes(暴击率)做随机判定,只有暴击成功时 flat_attributes(暴击伤害)和 multiplier_attributes 才会生效。目标的暴击抵抗属性(如 physical_crit_evasion)会降低暴击概率 |
defense / target_defense | 防御阶段。通常配合 source: TARGET、mode: SUBTRACT 使用,从目标身上读取防御属性来减免伤害 |
暴击阶段的工作方式
暴击判定的流程是:先把攻击者的暴击率减去目标的暴击抵抗,得到实际暴击概率,然后掷骰子。暴击成功后,暴击伤害值才会加到当前伤害上。目标的暴伤削减属性(如 physical_crit_multiplier_resistance)会进一步降低暴击伤害的加成幅度。
回复/吸血定义
伤害类型可以配置 recovery 块,让攻击者在造成伤害后回复生命值。
recovery:
source: ATTACKER # 吸血属性从攻击者身上读
resistance_source: TARGET # 吸血抵抗从目标身上读
flat_attributes: # 固定吸血值
- lifesteal
percent_attributes: # 百分比吸血(按最终伤害的百分比回复)
- percentage_lifesteal
resistance_attributes: # 目标的吸血抵抗
- lifesteal_resistance
expression: "" # 可选自定义表达式
min_result: 0.0 # 回复下限(不会出现负数回复)
max_result: null # 回复上限吸血计算公式:
基础回复 = flat + finalDamage × (percent / 100)
抵抗系数 = 1 - clamp(resistance / 100, 0, 1)
实际回复 = 基础回复 × 抵抗系数也就是说,如果攻击者有 10 点固定吸血和 20% 百分比吸血,打出 200 点伤害,基础回复就是 10 + 200 × 0.2 = 50。如果目标有 30% 吸血抵抗,实际回复就是 50 × 0.7 = 35。
完整伤害计算流程
从一次攻击事件触发到最终伤害生效,整个流程如下:
┌─────────────────────────────────────────────┐
│ 1. 事件触发(EntityDamageByEntityEvent) │
│ 取消原版伤害事件,接管伤害计算 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 2. 投射物快照 │
│ 弓箭等投射物在发射时会快照攻击者属性 │
│ 命中时用发射时的快照,而非当前属性 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 3. 确定伤害类型 │
│ 根据 allowed_damage_causes 匹配 │
│ 匹配不到就用 default_damage_type │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 4. 闪避判定 │
│ 根据目标的 dodge_chance 掷骰子 │
│ 闪避成功 → 跳过后续所有计算 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 5. 阶段式计算 │
│ 按 stages 列表顺序依次执行 │
│ 每个阶段的输出作为下一阶段的输入 │
│ ┌─ base_attack (ADD) │
│ ├─ crit (暴击判定 + 暴击伤害) │
│ └─ target_defense (SUBTRACT) │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 6. 触发 EmakiAttributeDamageEvent │
│ 其他插件可以监听这个事件,修改最终伤害 │
│ 也可以取消这次伤害 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 7. 应用伤害 │
│ a. 击退 + 受伤音效 │
│ b. 扣减目标生命值 │
│ c. 仇恨更新 │
│ d. 攻击冷却 │
│ e. 吸血回复 │
│ f. 伤害消息发送 │
│ g. 生命值同步到 Bukkit │
└─────────────────────────────────────────────┘投射物快照
投射物(弓箭等)在发射瞬间会快照攻击者当前的属性状态。命中时使用的是发射时的快照,而不是命中时的实时属性。这意味着如果玩家射出箭后脱掉武器,箭的伤害不会受影响——这是有意为之的设计,避免了"射出去的箭因为属性变化而伤害突变"的问题。
DamageContext 变量
DamageContext 是贯穿整个伤害计算流程的上下文对象,包含了这次伤害的所有信息。
| 字段 | 类型 | 说明 |
|---|---|---|
attacker | LivingEntity | 攻击者实体 |
target | LivingEntity | 目标实体 |
projectile | Projectile | 投射物(近战时为 null) |
cause | DamageCause | Bukkit 伤害原因 |
damageTypeId | String | 伤害类型 ID |
sourceDamage | double | 原始伤害值(事件原始值) |
baseDamage | double | 基础伤害值(进入阶段计算的初始值) |
attackerSnapshot | AttributeSnapshot | 攻击者属性快照 |
targetSnapshot | AttributeSnapshot | 目标属性快照 |
variables | DamageContextVariables | 扩展变量表 |
DamageContextVariables
扩展变量表里存放了一些控制伤害行为的开关和额外信息:
| 键 | 说明 |
|---|---|
cause / damage_cause | 伤害原因 |
allow_critical | 是否允许暴击 |
allow_target_dodge | 是否允许目标闪避 |
calculate_target_defense | 是否计算目标防御 |
trigger_mythic_on_damaged | 是否触发 MythicMobs 的 onDamaged 事件 |
mythic_skill | MythicMobs 技能名称 |
mythic_power | MythicMobs 技能威力 |
damage_type | 伤害类型覆盖(可以在运行时强制指定伤害类型) |
DamageResult
伤害计算完成后会生成 DamageResult 对象,包含 damageTypeId、finalDamage、critical(是否暴击)、roll(暴击随机数)、stageValues(各阶段的输出值列表)以及完整的 DamageContext 引用。如果你在监听 EmakiAttributeDamageEvent,可以通过 getDamageResult() 拿到这些数据。