跳转至

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:复合目标——双层串联

当一个实体同时满足 BFHurtTargetBFArmorMaterial 时,协议认为存在两层防护:外层护甲物品(胸甲等)和内层本体结构(实体自身的装甲模型)。

管线按固定顺序执行:

  1. 第一层:创建 BFArmorAdapter 包裹实体。调用护甲层的完整三步——modifyPenetration(爆反拦截)、resolvePenetration(跳弹判定)、calculateFinalDamage(残余伤害量)。
  2. 构造子上下文:通过 ctx.childContext(residualDmg, childPen) 生成新上下文。childPen 在护甲击穿时等于残余穿深,未击穿/跳弹时等于 0(表示弹头被护甲挡下,仅钝伤能穿透)。
  3. 第二层bfTarget(实体自身)基于子上下文执行完整管线——resolvePenetrationcalculateFinalDamagehurt
  4. 回调:在本体层的最终结果上触发。武器侧 handler 拿到的是经过双层判定的最终穿甲结果。

关键设计:无论护甲是否击穿,本体始终执行完整管线。 当护甲未击穿时,childCtx 穿深为 0,本体 resolvePenetration(穿深=0) 通常判定为 PENETRATED(自身血肉被 0 穿深"击穿"),但 calculateFinalDamage 中的三级模型会将残余伤害再打 65% 折扣。这意味着护甲未击穿时的钝伤要经过两层衰减——符合"冲击波通过护甲和肌肉才到达内脏"的物理直觉。

分支1:目标实现 BFHurtTarget

这是协议的核心分支。当目标实现 BFHurtTarget 且不满足分支0的条件(无 BFArmorMaterial 护甲)时进入。协议认为目标声明了"我知道如何响应终点弹道伤害"。

三段代码对应概念五步管线:

PenetrationResult result = tb.resolvePenetration(ctx);

result 是后续所有判断的唯一依据。回调不会重新推断"是否击穿",伤害计算也不会重新决定"是否跳弹"。因此护甲侧如果覆写 resolvePenetration,必须保证它返回的是本次命中的最终结论。

float finalDmg = tb.calculateFinalDamage(ctx, result);

默认实现只在 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),命中后精确委托给单件护甲物品。无命中点时走 resolveBestSlotgetRHA/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() 可以安全获取 PlayerZombie 实例。

分支3:普通 Entity

如果目标不是 BFHurtTarget,也不是穿戴协议护甲的 LivingEntity,但它是普通 Entity

return entity.hurt(ctx.source(), ctx.baseDamage())
    ? ctx.baseDamage()
    : 0f;

这条路径不会调用 resolvePenetration,不会触发 BFDamageHandler 回调,也不会读取 extensions。对未参与协议的实体来说,这次调用与普通原版伤害几乎没有区别。武器侧可以放心地统一调用 BFDamageApi.hurt(),不必先判断目标是否支持协议。

概念五步与代码三段

文档中常说的五步管线是协议的概念模型:

  1. getRHA(ctx):获取命中部位的 RHA 等效厚度。
  2. modifyPenetration(ctx):让护甲侧对穿深做额外减效。
  3. resolvePenetration(ctx):给出 PENETRATEDBLOCKEDRICOCHET
  4. calculateFinalDamage(ctx, result):把标称伤害换算为最终伤害。
  5. 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 中显式调用 getRHAmodifyPenetration

@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 时,协议会在伤害执行之后调用回调:

if (handler != null) {
    triggerCallbacks(handler, tb, ctx, result);
}

回调发生在 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,才触发 onOvermatchonSpall

回调是否触发不取决于 dealt 是否大于 0。即使最终伤害为 0,主结果回调和由 handler 判定为 true 的派生回调仍然会触发,因为武器侧通常仍然需要播放撞击、跳弹或破片效果。

返回值的语义

BFDamageApi.hurt 返回的是"实际造成的伤害量",不是"理论最终伤害"。

对于分支0和分支1(BFHurtTarget),返回值只有在两个条件同时满足时才等于 finalDmgcalculateFinalDamage 返回正数,并且目标的 hurt(source, finalDmg) 返回 true。只要最终伤害为 0,或目标因为原版机制、无敌帧、事件取消等原因拒绝伤害,返回值就是 0。

对于分支2(适配器),adapter.hurt 委托 entity.hurt——原版护甲减免在此之上叠加。返回值是原版 hurt 的出参,并非协议层的最初计算值。

对于分支3(普通 Entity),返回值是 baseDamage 或 0。协议不会知道原版内部是否进一步减免了伤害。

管线边界

协议管线刻意不做几件事。

它不直接修改实体生命值,而是通过 hurt(source, amount) 委托给目标。这样可以保留原版伤害事件、无敌帧、抗性、死亡处理等机制。

它不内置弹药物理模型。角度修正、速度衰减、弹种差异主要由武器侧在构造 BFDamageContext 前完成;爆反、间隙、部位弱点等目标侧减效逻辑则放在护甲侧。

它不要求所有目标都加入协议。普通实体仍然走原版路径(分支3),协议感知实体才进入穿甲判定。护甲物品通过适配器模式(分支2)让原版实体也能零代码接入管线——这是 BallisticsFramework 能作为 lib 模组存在的关键:它连接参与者,而不是接管整个战斗系统。