3.5 协议外伤害兼容¶
当一个实体实现了 BFHurtTarget,它不仅会响应 BFDamageApi.hurt() 发起的协议伤害,还会被协议的 Mixin 注入所拦截——原版的生物攻击、TNT 爆炸、投射物、魔法伤害等都可能被转换为协议伤害。createContextFromVanilla 方法决定了哪些原版伤害走协议管线、哪些退回原版流程。正确地实现此方法对模组的兼容性和性能都至关重要。
Mixin 拦截的真实行为¶
协议通过两个 Mixin 注入类(EntityHurtMixin 和 LivingEntityHurtMixin)拦截所有的 Entity#hurt 调用。拦截逻辑分四步走:
第一步:重入检查。 如果当前线程已经有针对该目标的协议上下文(即已经处于协议管线内),拦截器放行——这说明这次 hurt 调用是协议管线内部发出的(第五步 target.hurt()),不应重复拦截。
第二步:BFHurtTarget 检查。 如果目标实现了 BFHurtTarget,拦截器调用 tb.createContextFromVanilla(source, amount)。如果返回 null,继续下一步;如果返回了一个上下文,拦截器调用 BFDamageApi.hurt(self, ctx) 并将 Mixin 的返回值设为 true。本章后续内容全部围绕此情况展开。
第三步:BFArmorMaterial 检查。 如果目标没有实现 BFHurtTarget,但它是 LivingEntity 且穿戴了 BFArmorMaterial 护甲,拦截器通过 BFArmorAdapter 自动构造上下文并调用 BFDamageApi.hurt()。适配器的 createContextFromVanilla 始终返回有效上下文——这意味着穿戴了协议护甲的实体自动将所有原版伤害纳入穿甲判定,无需手动筛选。详见 3.6 BFArmorMaterial 接口 中关于协议外伤害兼容的讨论。
第四步:放行。 以上条件均不满足——普通实体,走原版流程。
理解这个流程对正确实现 createContextFromVanilla 至关重要。它说明了两件事:你在 hurt 方法内调用的 super.hurt() 不会触发二次拦截(因为此时已在管线内);你在 createContextFromVanilla 中的判断决定了哪些原版伤害被"升级"为协议伤害。
策略一:返回 null(推荐起步方式)¶
对于大多数护甲模组,最安全且最简洁的策略是在 createContextFromVanilla 中直接返回 null。这意味着只有通过 BFDamageApi.hurt() 显式发起的协议伤害才走穿甲判定,所有原版来源的伤害保持原样:
@Override
@Nullable
public BFDamageContext createContextFromVanilla(DamageSource source, float amount) {
return null;
}
这种策略的好处是可以让你分阶段开发:首先实现简易模式的穿甲判定(getArmorLevel + hurt),让枪械模组的协议伤害正确运行;等这个路径稳定后,再逐步将某些原版伤害也纳入协议管线。
返回 null 时,协议层的存在对游戏行为没有影响。一个实现了 BFHurtTarget 且返回 null 的僵尸被另一个僵尸攻击时,伤害判定与原版完全相同。只有在枪械模组通过 BFDamageApi.hurt() 攻击它时,才会走穿甲管线。
策略二:筛选后转换¶
当你的装甲系统成熟后,你可能希望部分原版伤害也受装甲减免。最常见的需求是:原版生物的物理攻击应该被装甲抵挡,但环境伤害(岩浆、溺水、虚空)和魔法伤害(女巫的药水)不应该被装甲影响。
你可以在 createContextFromVanilla 中根据 DamageSource 类型做筛选:
@Override
@Nullable
public BFDamageContext createContextFromVanilla(DamageSource source, float amount) {
// 以下类型的伤害完全不适合走穿甲管线,直接退回原版
if (source.is(DamageTypes.OUT_OF_WORLD) // 虚空
|| source.is(DamageTypes.DROWN) // 溺水
|| source.is(DamageTypes.STARVE) // 饥饿
|| source.is(DamageTypes.MAGIC) // 魔法
|| source.is(DamageTypes.WITHER) // 凋零
|| source.is(DamageTypes.ON_FIRE) // 着火
|| source.is(DamageTypes.HOT_FLOOR) // 岩浆块
|| source.is(DamageTypes.LAVA)) { // 岩浆
return null;
}
// 物理攻击(僵尸、骷髅、铁傀儡等)、投射物、爆炸——这些走穿甲判定
// 构造低信息量上下文
return BFDamageContext.builder()
.source(source)
.baseDamage(amount)
.build();
}
筛选列表的取舍取决于你的游戏设计。有些模组可能希望爆炸伤害也走穿甲判定(坦克应能承受 TNT 爆炸),有些则希望爆炸永远绕过装甲(模拟冲击波直接伤害内部)。返回 null 的伤害类型越多,协议对游戏行为的改变越小;转换的伤害类型越多,装甲的一致性越强。
低信息量上下文的行为¶
当你在 createContextFromVanilla 中构造上下文时,通常只能设置 source 和 baseDamage——原版伤害没有弹道信息。其余字段使用 Builder 的默认值:penetration 为 0,hitVelocity 为零向量,hitNormal 朝上。
这会导致什么?穿深为 0 的伤害经过 getPenetrationLevel() 映射到 UNARMORED_1。在简易模式的默认判定中,UNARMORED_1.canDefeat(UNARMORED_1) 返回 true(等于算击穿),因此同为"无甲"的目标会被击穿;而任何有装甲的目标(LIGHT_1 及以上)都不会被击穿——原版生物攻击无法穿透装甲。
这个行为往往是期望的。一个僵尸攻击坦克应该不造成伤害,因为它没有穿甲能力。但在 calculateFinalDamage 中,你仍然可以覆写为返回非零钝伤——让大口径原版爆炸对装甲目标造成一定程度的冲击伤害。
如果你希望返回更丰富的上下文,可以基于 source 的类型做推断。例如,爆炸伤害可以基于爆炸中心位置估算一个命中法线:
if (source.is(DamageTypes.EXPLOSION) || source.is(DamageTypes.PLAYER_EXPLOSION)) {
Vec3 explosionPos = source.getSourcePosition();
if (explosionPos != null) {
Vec3 toEntity = this.position().subtract(explosionPos).normalize();
// 爆炸方向视为命中法线的反方向
return BFDamageContext.builder()
.source(source)
.baseDamage(amount)
.hitPoint(this.position()) // 近似为实体中心
.hitNormal(toEntity.scale(-1)) // 法线指向爆炸中心方向(面外侧)
.penetration(20f) // 假设爆炸破片有 20mm 穿深
.build();
}
}
这种推断虽不精确,但比纯零信息量上下文能提供更合理的判定——至少跳弹和角度相关的判定可以有正确的方向参考。
与扩展模组的协作¶
如果你的护甲模组实现了 createContextFromVanilla 并对某些原版伤害进行转换,需要注意与其他也实现了 BFHurtTarget 的模组的交互。因为 createContextFromVanilla 是你类中的覆写,不会影响其他实体的行为。不同的 BFHurtTarget 实现者可以有不同的转换策略——一个坦克可能将爆炸转换为协议伤害,一个魔法傀儡可能连物理攻击也不转换。
但如果你的模组期望其他护甲模组也按照你的逻辑来转换原版伤害,这是做不到的——createContextFromVanilla 的覆写权属于每个实体类自身,不存在全局配置。如果你需要统一所有 BFHurtTarget 的转换行为,需要与各护甲模组作者逐个协调,或在你的文档中说明推荐策略。