4.1 穿甲判定管线¶
前面的章节把协议伤害描述为一个固定的流程——获取装甲等效厚度、修正穿深、解析穿甲结果、计算最终伤害、执行伤害。本节从内部实现的角度重新看这条管线,重点解释 BFDamageApi.hurt() 实际做了什么、四个分支如何调度、以及 BFArmorMaterial 的适配器如何融入管线。
入口:BFDamageApi.hurt 的四个分支¶
所有协议伤害都从 BFDamageApi.hurt(target, ctx) 进入。这个方法承担三个职责:把上下文压入当前线程的上下文栈;按优先级判定目标类型并进入相应分支;在管线结束后触发武器侧回调并弹出上下文。
核心结构可以简化为下面这样:
public static float hurt(Object target, BFDamageContext ctx) {
BFDamageHandler handler = ctx.getHandler();
Object stackTarget = ...; // 栈 target 选取规则见 4.3 节
TBContextStack.INSTANCE.push(stackTarget, ctx);
try {
// 分支0:复合目标 — BFHurtTarget + BFArmorMaterial 护甲
if (target instanceof LivingEntity living
&& target instanceof BFHurtTarget tb
&& BFArmorAdapter.hasBFArmor(living)) {
BFArmorAdapter adapter = new BFArmorAdapter(living);
float residualPen = adapter.modifyPenetration(ctx);
PenetrationResult armorResult = adapter.resolvePenetration(ctx);
float residualDmg = adapter.calculateFinalDamage(ctx, armorResult);
// 构造子上下文——未击穿/跳弹时穿深传 0 表示仅钝伤
float childPen = armorResult == PENETRATED ? residualPen : 0f;
BFDamageContext childCtx = ctx.childContext(residualDmg, childPen);
PenetrationResult entityResult = tb.resolvePenetration(childCtx);
float finalDmg = tb.calculateFinalDamage(childCtx, entityResult);
boolean success = tb.hurt(ctx.source(), finalDmg);
float dealt = success ? finalDmg : 0f;
if (handler != null) {
triggerCallbacks(handler, tb, childCtx, entityResult);
}
return dealt;
}
// 分支1:BFHurtTarget(实体或独立对象)
if (target instanceof BFHurtTarget tb) {
PenetrationResult result = tb.resolvePenetration(ctx);
float finalDmg = tb.calculateFinalDamage(ctx, result);
boolean success = finalDmg > 0f && tb.hurt(ctx.source(), finalDmg);
float dealt = success ? finalDmg : 0f;
if (handler != null) {
triggerCallbacks(handler, tb, ctx, result);
}
return dealt;
}
// 分支2:LivingEntity 穿戴了 BFArmorMaterial 护甲 → 适配器模式
if (target instanceof LivingEntity living && BFArmorAdapter.hasBFArmor(living)) {
BFArmorAdapter adapter = new BFArmorAdapter(living);
PenetrationResult result = adapter.resolvePenetration(ctx);
float finalDmg = adapter.calculateFinalDamage(ctx, result);
boolean success = finalDmg > 0f && adapter.hurt(ctx.source(), finalDmg);
float dealt = success ? finalDmg : 0f;
if (handler != null) {
triggerCallbacks(handler, adapter, ctx, result);
}
return dealt;
}
// 分支3:普通 Entity → 原版回退
if (target instanceof Entity entity) {
return entity.hurt(ctx.source(), ctx.baseDamage()) ? ctx.baseDamage() : 0f;
}
return 0f;
} finally {
TBContextStack.INSTANCE.pop();
}
}
分支按优先级依次判断——分支0(复合)优先于分支1(BFHurtTarget)。这意味着一个同时满足所有条件的实体(它自己实现了 BFHurtTarget,又是 LivingEntity,还穿了 BFArmorMaterial 护甲)一定会进入复合分支。纯 BFHurtTarget(无护甲物品)走分支1,纯护甲物品(无 BFHurtTarget)走分支2。
分支0:复合目标——双层串联¶
当一个实体同时满足 BFHurtTarget 和 BFArmorMaterial 时,协议认为存在两层防护:外层护甲物品(胸甲等)和内层本体结构(实体自身的装甲模型)。
管线按固定顺序执行:
- 第一层:创建
BFArmorAdapter包裹实体。调用护甲层的完整三步——modifyPenetration(爆反拦截)、resolvePenetration(跳弹判定)、calculateFinalDamage(残余伤害量)。 - 构造子上下文:通过
ctx.childContext(residualDmg, childPen)生成新上下文。childPen在护甲击穿时等于残余穿深,未击穿/跳弹时等于 0(表示弹头被护甲挡下,仅钝伤能穿透)。 - 第二层:
bfTarget(实体自身)基于子上下文执行完整管线——resolvePenetration、calculateFinalDamage、hurt。 - 回调:在本体层的最终结果上触发。武器侧 handler 拿到的是经过双层判定的最终穿甲结果。
关键设计:无论护甲是否击穿,本体始终执行完整管线。 当护甲未击穿时,childCtx 穿深为 0,本体 resolvePenetration(穿深=0) 通常判定为 PENETRATED(自身血肉被 0 穿深"击穿"),但 calculateFinalDamage 中的三级模型会将残余伤害再打 65% 折扣。这意味着护甲未击穿时的钝伤要经过两层衰减——符合"冲击波通过护甲和肌肉才到达内脏"的物理直觉。
分支1:目标实现 BFHurtTarget¶
这是协议的核心分支。当目标实现 BFHurtTarget 且不满足分支0的条件(无 BFArmorMaterial 护甲)时进入。协议认为目标声明了"我知道如何响应终点弹道伤害"。
三段代码对应概念五步管线:
result 是后续所有判断的唯一依据。回调不会重新推断"是否击穿",伤害计算也不会重新决定"是否跳弹"。因此护甲侧如果覆写 resolvePenetration,必须保证它返回的是本次命中的最终结论。
默认实现只在 PENETRATED 时造成伤害:同级击穿为 baseDamage * 0.65f,越级击穿为 baseDamage,未击穿和跳弹为 0。这里没有把"未击穿"写死为无伤害,护甲侧可以覆写此方法来实现钝伤、震伤、模块损坏或跳弹擦伤。
boolean success = finalDmg > 0f && tb.hurt(ctx.source(), finalDmg);
float dealt = success ? finalDmg : 0f;
如果 finalDmg <= 0,协议不会调用 tb.hurt。这避免了大量 0 伤害命中进入原版管线,触发无意义的受击动画、无敌帧或其他副作用。若你希望未击穿也触发某种实体后果,应在 calculateFinalDamage 返回一个非零值,或在回调/自定义方法中处理非伤害效果。
分支2:适配器模式——BFArmorMaterial 护甲¶
当目标是 LivingEntity,穿戴了 BFArmorMaterial 护甲,但自身不是 BFHurtTarget 时进入。协议创建 BFArmorAdapter 将实体包裹为 BFHurtTarget 实现,使管线能查询装备槽位中的护甲防护数据。
适配器的关键行为:
- 槽位解析:
adapter.ensureResolved(ctx)惰性解析命中槽位。有命中点时走resolveTarget(逐槽位匹配mapHitToSlot),命中后精确委托给单件护甲物品。无命中点时走resolveBestSlot:getRHA/getArmorLevel返回四槽位加权平均(胸甲35%、头盔30%、护腿25%、靴子10%),modifyPenetration/resolvePenetration/calculateFinalDamage则委托最高等级槽位的护甲物品。 hurt(source, amount)→ 直接委托entity.hurt(source, amount)走原版伤害管线。适配器自身不进入上下文栈——BFDamageApi.hurt()中的stackTarget在分支2是实体自身(因为 target 不是BFHurtTarget),与entity.hurt()的this为同一引用,hasContextFor放行。
适配器的 getBFEntity() 返回被包裹的实体引用,因此武器侧 handler 通过 target.getBFEntity() 可以安全获取 Player 或 Zombie 实例。
分支3:普通 Entity¶
如果目标不是 BFHurtTarget,也不是穿戴协议护甲的 LivingEntity,但它是普通 Entity:
这条路径不会调用 resolvePenetration,不会触发 BFDamageHandler 回调,也不会读取 extensions。对未参与协议的实体来说,这次调用与普通原版伤害几乎没有区别。武器侧可以放心地统一调用 BFDamageApi.hurt(),不必先判断目标是否支持协议。
概念五步与代码三段¶
文档中常说的五步管线是协议的概念模型:
getRHA(ctx):获取命中部位的 RHA 等效厚度。modifyPenetration(ctx):让护甲侧对穿深做额外减效。resolvePenetration(ctx):给出PENETRATED、BLOCKED或RICOCHET。calculateFinalDamage(ctx, result):把标称伤害换算为最终伤害。hurt(source, amount):真正执行伤害。
但在代码入口中,BFDamageApi 不会逐个调用前两个方法,而是只调用 resolvePenetration(ctx)、calculateFinalDamage(ctx, result) 和 hurt(source, amount)。原因是前两个方法本质上是穿甲判定的子步骤,应该由护甲侧的 resolvePenetration 实现自由组织。
默认实现采用离散等级模式,因此 resolvePenetration 只委托给 isArmorPenetrated(ctx):
default PenetrationResult resolvePenetration(BFDamageContext ctx) {
return isArmorPenetrated(ctx)
? PenetrationResult.PENETRATED
: PenetrationResult.BLOCKED;
}
如果你的护甲侧需要精密数值判定,就应在自己的 resolvePenetration 中显式调用 getRHA 和 modifyPenetration:
@Override
public PenetrationResult resolvePenetration(BFDamageContext ctx) {
float rha = getRHA(ctx);
float pen = modifyPenetration(ctx);
if (shouldRicochet(ctx, pen, rha)) {
return PenetrationResult.RICOCHET;
}
return pen >= rha
? PenetrationResult.PENETRATED
: PenetrationResult.BLOCKED;
}
这样设计的好处是,协议层只规定"你必须给出一个互斥的最终结果",但不强制护甲系统采用哪一种物理模型。简单模组可以只实现 getArmorLevel,精密模组可以把斜面、爆反、间隙、部位弱点全部写进自己的判定逻辑。无论是分支0、分支1还是分支2,resolvePenetration 始终是穿甲判定的唯一出口。
回调发生在伤害之后¶
当上下文中带有 BFDamageHandler 时,协议会在伤害执行之后调用回调:
回调发生在 finally 弹栈之前,因此此时 BFDamageApi.getContextFor(target) 仍然能读到当前上下文。这对需要在回调中访问命中点、法线、扩展数据的武器侧逻辑很有用。
在分支0(复合目标)中,回调传入的是本体的最终结果(entityResult)而非护甲层的中间结果。武器侧拿到的是一次完整的双层判定结论。在分支2(适配器)中,回调传入的是适配器实例——handler 通过 target.getBFEntity() 获取被包裹实体。
回调顺序分为两段。第一段由 result 决定主结果回调:
| 穿甲结果 | 主回调 |
|---|---|
PENETRATED |
onPenetrated |
BLOCKED |
onBlocked |
RICOCHET |
onRicochet |
第二段由武器侧 handler 决定派生效果回调。协议会分别调用 isOvermatch(target, ctx, result) 和 isSpall(target, ctx, result);对应方法返回 true,才触发 onOvermatch 或 onSpall。
回调是否触发不取决于 dealt 是否大于 0。即使最终伤害为 0,主结果回调和由 handler 判定为 true 的派生回调仍然会触发,因为武器侧通常仍然需要播放撞击、跳弹或破片效果。
返回值的语义¶
BFDamageApi.hurt 返回的是"实际造成的伤害量",不是"理论最终伤害"。
对于分支0和分支1(BFHurtTarget),返回值只有在两个条件同时满足时才等于 finalDmg:calculateFinalDamage 返回正数,并且目标的 hurt(source, finalDmg) 返回 true。只要最终伤害为 0,或目标因为原版机制、无敌帧、事件取消等原因拒绝伤害,返回值就是 0。
对于分支2(适配器),adapter.hurt 委托 entity.hurt——原版护甲减免在此之上叠加。返回值是原版 hurt 的出参,并非协议层的最初计算值。
对于分支3(普通 Entity),返回值是 baseDamage 或 0。协议不会知道原版内部是否进一步减免了伤害。
管线边界¶
协议管线刻意不做几件事。
它不直接修改实体生命值,而是通过 hurt(source, amount) 委托给目标。这样可以保留原版伤害事件、无敌帧、抗性、死亡处理等机制。
它不内置弹药物理模型。角度修正、速度衰减、弹种差异主要由武器侧在构造 BFDamageContext 前完成;爆反、间隙、部位弱点等目标侧减效逻辑则放在护甲侧。
它不要求所有目标都加入协议。普通实体仍然走原版路径(分支3),协议感知实体才进入穿甲判定。护甲物品通过适配器模式(分支2)让原版实体也能零代码接入管线——这是 BallisticsFramework 能作为 lib 模组存在的关键:它连接参与者,而不是接管整个战斗系统。