3.1 实现 BFHurtTarget¶
BFHurtTarget 是护甲侧的核心接口。任何一个希望参与协议穿甲判定的实体,都必须实现此接口。接口共定义 8 个方法——3 个 abstract(你必须实现),5 个 default(你可选覆写)。对于简单模组,你只需要实现 getArmorLevel、hurt 和 createContextFromVanilla 三个方法,协议就会自动完成余下的所有工作。
三个必须实现的方法¶
首先看完整的接口骨架。以下是让一个自定义实体成为协议感知目标的最简代码:
public class ArmoredGolem extends IronGolem implements BFHurtTarget {
/* ========== 管线方法 —— 必须实现 ========== */
@Override
public ArmorLevel getArmorLevel(BFDamageContext ctx) {
// 基于命中部位返回护甲等级(见 3.2 节)
return determineArmorLevelFromHitPoint(ctx.hitPoint());
}
@Override
public boolean hurt(DamageSource source, float amount) {
// amount 是经协议 calculateFinalDamage 计算后的最终伤害量
// 通常委托给原版管线
return super.hurt(source, amount);
}
@Override
@Nullable
public BFDamageContext createContextFromVanilla(DamageSource source, float amount) {
// 原版伤害如何处理?返回 null = 走原版流程
// 详见 3.5 节
return null;
}
}
这三个方法的作用截然不同。getArmorLevel 是管线的起点——协议在收到一次伤害请求后,首先调用它来了解目标在此命中部位的防护能力。hurt 是管线的终点——协议在完成穿甲判定和伤害计算后,将最终伤害量交给你执行。createContextFromVanilla 则是管线的另一类入口——当原版来源的伤害(如僵尸攻击)命中你时,是否应转换为协议伤害来走穿甲判定。
五个可选覆写的 default 方法¶
除了上述三个 abstract 方法,接口还提供了 5 个带 default 实现的方法。下面简要说明每个方法的默认行为和覆写场景,后续各节将逐一展开。
| 方法 | 默认行为 | 覆写场景 |
|---|---|---|
getRHA(ctx) |
取 getArmorLevel(ctx).medianRha() |
需要毫米级精度的等效厚度时(见 3.2 节) |
modifyPenetration(ctx) |
直接返回 ctx.penetration() |
需要爆反拦截、间隙衰减等减效逻辑时(见 3.3 节) |
resolvePenetration(ctx) |
基于 isArmorPenetrated 返回 PENETRATED/BLOCKED |
需要跳弹判定(入射角过大 → RICOCHET)时(见 3.4 节) |
calculateFinalDamage(ctx, result) |
三级模型:同级击穿 65%,越级 100%,未击穿 0 | 需要钝伤、超匹配加成等自定义伤害时(见 3.4 节) |
isArmorPenetrated(ctx) |
等级比较 >=(等于算击穿) |
通常不需单独覆写——覆写 resolvePenetration 即可完全接管判定逻辑 |
你可以把这些 default 方法理解为"逐层增强的钩子"。简单模组一行 default 都不需要覆写——只实现三个 abstract 方法就足够。需要某一步的逻辑精细化时,只覆写那一步,其余步骤保持默认。
五步管线的完整调用¶
理解协议在内部如何调用这些方法,有助于正确覆写。管线的完整调用顺序是:
第一步:target.getRHA(ctx) —— 获取命中部位的 RHA 等效厚度。默认实现取 getArmorLevel(ctx).medianRha()。覆写此方法可返回精确 float 值。注意即使覆写了 getRHA,也应保持 getArmorLevel 的正确实现——因为默认的 calculateFinalDamage 仍依赖等级做同级/越级判定,且等级也用于 HUD 显示。最简单的做法是在 getArmorLevel 中调用 ArmorLevel.fromRha(getRHA(ctx)),让等级始终与你覆写的精确 RHA 值保持一致。
第二步:target.modifyPenetration(ctx) —— 护甲侧的穿深修正。默认直接返回 ctx.penetration()(武器侧已完成角度修正)。覆写此方法可加入爆反拦截、间隙衰减等减效计算。
第三步:target.resolvePenetration(ctx) —— 穿甲判定,返回 PenetrationResult(PENETRATED/BLOCKED/RICOCHET)。默认实现委托 isArmorPenetrated(等级比较),仅区分击穿/未击穿。覆写此方法可加入跳弹判定。此方法被管线单次调用,其返回值作为后续所有步骤的依据。
第四步:target.calculateFinalDamage(ctx, result) —— 根据穿甲结果计算最终伤害。默认三级模型基于等级比较。覆写可实现钝伤、超匹配加成等。
第五步:target.hurt(source, finalDmg) —— 执行伤害。对于 LivingEntity 子类,通常委托 super.hurt(source, finalDmg) 走原版伤害管线(护甲计算、附魔、无敌帧等)。
第三步的返回值极其关键——被阻挡的结果(BLOCKED/RICOCHET)会继续进入第四步(calculateFinalDamage 此时可返回非零钝伤),然后进入第五步。这意味着护甲侧可以在"被挡住了"时仍然让目标承受一定的钝伤,这取决你如何覆写 calculateFinalDamage。
简易模式 vs 精密模式¶
当你仅实现三个 abstract 方法(getArmorLevel、hurt、createContextFromVanilla)而不覆写任何 default 方法时,你处于简易模式。此模式下协议的判定逻辑全部基于离散的 ArmorLevel 等级体系——等级比较用 >=(等于算击穿),伤害用三级模型(0/65%/100%),无跳弹判定,无穿深修正。
简易模式适合"这个怪有三级甲"或"这个坦克正面是五级甲"的场景——你只需关心"什么等级",协议帮你处理"能不能打穿"。对于大多数以游戏性为导向的护甲模组,简易模式已经足够。
当你开始覆写 default 方法时,你进入了精密模式。精密模式不要求一次性覆写全部方法——你可以逐步深入。最常见的路径是:先覆写 getRHA 引入精确浮点厚度(但仍保持等级体系作为兜底),再覆写 resolvePenetration 引入跳弹判定(基于入射角),最后覆写 calculateFinalDamage 引入钝伤模型。
你不需要在两种模式之间做二选一的决定——BFHurtTarget 的 default 方法设计允许你渐进式地加深定制程度。这在多人模组协作时特别有用:你开发一个基础的装甲实体用简易模式,另一个开发者可以通过 Mixin 注入或子类化来覆写管线方法,添加更复杂的物理模型。
在管线方法内获取更多信息¶
在管线的任何方法内,你都可以通过 BFDamageApi.getContextFor(this) 获取当前上下文。不过大多数管线方法(getArmorLevel、getRHA、modifyPenetration、resolvePenetration、calculateFinalDamage)已经将 ctx 作为参数传入,通常不需要额外调用 getContextFor。最需要使用 getContextFor 的地方是 hurt 方法——它没有 ctx 参数,因为方法签名必须与 LivingEntity#hurt 兼容。
在 hurt 内获取上下文可以做更丰富的反馈——例如根据命中点在 entity 的哪个部位来播放不同的受伤动画:
@Override
public boolean hurt(DamageSource source, float amount) {
BFDamageContext ctx = BFDamageApi.getContextFor(this);
if (ctx != null) {
Vec3 localHit = ctx.hitPoint().subtract(this.position());
if (localHit.y > this.getEyeHeight()) {
// 命中头部——额外伤害反馈
this.level().broadcastDamageEvent(this, DamageEventType.HEADSHOT);
amount *= 1.5f;
}
}
return super.hurt(source, amount);
}
不委托 super.hurt() 的自定义处理¶
如果你的实体不完全依赖原版管线处理伤害——例如模块化载具需要将伤害分配到特定子部件、或完全自定义的伤害系统——hurt 就不能简单地委托 super.hurt()。此时你需要自行区分"此伤害来自协议还是原版",并在两种路径下做不同处理。
判断伤害来源¶
BFDamageApi.getContextFor(this) 返回非 null 时,说明此次 hurt 调用来自协议管线内部——协议已完成穿甲判定和伤害计算,amount 是 calculateFinalDamage 的输出。此时你应直接执行伤害,不再走穿甲管线:
@Override
public boolean hurt(DamageSource source, float amount) {
BFDamageContext ctx = BFDamageApi.getContextFor(this);
if (ctx != null) {
// 协议伤害——amount 已经过穿甲判定和伤害计算,直接应用
this.customHealth -= amount;
if (ctx.hitPoint() != null) {
spawnHitEffects(ctx.hitPoint()); // 在命中点播放特效
}
return true;
}
// 协议外伤害——可能是 createContextFromVanilla 返回 null 放行的原版伤害
// 根据你的设计决定如何处理(走原版、忽略、或在此处转换)
return super.hurt(source, amount);
}
协议外伤害不应在 hurt 内重新发起协议伤害¶
一个关键的误区是在 hurt 中检测到无上下文后,自行调用 BFDamageApi.hurt(this, ctx) 来"补救":
// ❌ 错误:在 hurt 内调用 BFDamageApi.hurt() 会导致无限递归
public boolean hurt(DamageSource source, float amount) {
if (BFDamageApi.getContextFor(this) == null) {
BFDamageContext ctx = createContextFromVanilla(source, amount);
if (ctx != null) {
return BFDamageApi.hurt(this, ctx) > 0; // 又进入 hurt → 再次检测到无上下文 → 死循环!
}
}
...
}
这会导致无限递归——BFDamageApi.hurt() 内部会再次调用 this.hurt(),而在协议推送上下文之前实例尚未进入管线,getContextFor 仍返回 null,循环开始。
正确做法是根据你的设计选择以下之一:
- 让 Mixin 拦截器替你做:在
createContextFromVanilla中为想要接管的伤害类型返回上下文(Mixin 会自动调用BFDamageApi.hurt()),不想要的返回 null。此时 hurt 内只需按上方的 ctx 判断分流。 - 在 hurt 中直接执行原版回退:对于 createContextFromVanilla 返回 null 的伤害,在 hurt 的"无 ctx"分支直接调用
super.hurt()或setHealth(),不再经过协议管线。
完整示例:模块化伤害系统¶
以下展示一个不使用 super.hurt() 的实体如何正确处理协议伤害和原版伤害:
public class ModularTank extends LivingEntity implements BFHurtTarget {
private final Map<String, SubPart> parts = new HashMap<>();
private float hullIntegrity = 100f;
@Override
public boolean hurt(DamageSource source, float amount) {
BFDamageContext ctx = BFDamageApi.getContextFor(this);
if (ctx != null) {
// 协议伤害:已在管线中完成穿甲判定,amount 是最终伤害量
// 根据命中点将伤害分配到对应子部件
SubPart hitPart = findPartAt(ctx.hitPoint());
if (hitPart != null) {
hitPart.applyDamage(amount);
}
// 残余伤害作用到车体
this.hullIntegrity -= amount * 0.2f;
return true;
}
// 协议外伤害——由 createContextFromVanilla 控制哪些类型进入协议
// 未转换的伤害直接作用到车体(如环境伤害)
this.hullIntegrity -= amount;
return true;
}
@Override
@Nullable
public BFDamageContext createContextFromVanilla(DamageSource source, float amount) {
// 爆炸、投射物——走穿甲判定
if (source.is(DamageTypes.EXPLOSION)
|| source.is(DamageTypes.ARROW)) {
return BFDamageContext.builder()
.source(source).baseDamage(amount)
.penetration(estimateFromSource(source, amount))
.build();
}
// 魔法、虚空、溺水——直接作用到车体,不经过穿甲判定
return null;
}
// ...其余管线方法(getArmorLevel等)
}
关键点:
- ctx != null → 协议伤害,amount 已是最终值,直接应用
- ctx == null → 协议外伤害(被你主动过滤掉的类型),按原版逻辑处理
- 绝不在 hurt 中调用 BFDamageApi.hurt()
目标不是 Entity 的情况¶
BFHurtTarget 不强制实现者必须是 Entity 的子类——方法签名中使用的是 DamageSource 和 BFDamageContext,不直接引用 Entity。然而,在实际的 Minecraft 模组开发中,实现 BFHurtTarget 的类几乎总是 LivingEntity 的子类(僵尸、玩家、自定义生物等),因为只有实体才能被 Entity#hurt 拦截。
对于非 Entity 的目标(例如一个逻辑上的装甲块代表、模块化的车辆部件),你可以在 hurt 方法中完全不调用 super.hurt,而是自己维护伤害计数或破坏状态。此时你仍然可以使用协议的穿甲判定管线——getArmorLevel、resolvePenetration、calculateFinalDamage 会正常执行,只是最后的 hurt 由你自定义处理。
还有另一种方式:BFArmorMaterial¶
如果你的目标是给原版实体(玩家、僵尸等)添加防护——你无法修改它们的类来让它们实现 BFHurtTarget——协议提供了另一条路径:让护甲物品实现 BFArmorMaterial 接口。胸甲、头盔、护腿、靴子都可以通过实现此接口自动为穿戴者提供穿甲判定能力,零事件订阅、零 Manually Mixin。
关于两条路径的对比、BFArmorMaterial 的简易/精密模式、槽位映射、双层串联管线等,见 3.6 BFArmorMaterial 接口。