跳转至

3.1 实现 BFHurtTarget

BFHurtTarget 是护甲侧的核心接口。任何一个希望参与协议穿甲判定的实体,都必须实现此接口。接口共定义 8 个方法——3 个 abstract(你必须实现),5 个 default(你可选覆写)。对于简单模组,你只需要实现 getArmorLevelhurtcreateContextFromVanilla 三个方法,协议就会自动完成余下的所有工作。

三个必须实现的方法

首先看完整的接口骨架。以下是让一个自定义实体成为协议感知目标的最简代码:

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 方法(getArmorLevelhurtcreateContextFromVanilla)而不覆写任何 default 方法时,你处于简易模式。此模式下协议的判定逻辑全部基于离散的 ArmorLevel 等级体系——等级比较用 >=(等于算击穿),伤害用三级模型(0/65%/100%),无跳弹判定,无穿深修正。

简易模式适合"这个怪有三级甲"或"这个坦克正面是五级甲"的场景——你只需关心"什么等级",协议帮你处理"能不能打穿"。对于大多数以游戏性为导向的护甲模组,简易模式已经足够。

当你开始覆写 default 方法时,你进入了精密模式。精密模式不要求一次性覆写全部方法——你可以逐步深入。最常见的路径是:先覆写 getRHA 引入精确浮点厚度(但仍保持等级体系作为兜底),再覆写 resolvePenetration 引入跳弹判定(基于入射角),最后覆写 calculateFinalDamage 引入钝伤模型。

你不需要在两种模式之间做二选一的决定——BFHurtTarget 的 default 方法设计允许你渐进式地加深定制程度。这在多人模组协作时特别有用:你开发一个基础的装甲实体用简易模式,另一个开发者可以通过 Mixin 注入或子类化来覆写管线方法,添加更复杂的物理模型。

在管线方法内获取更多信息

在管线的任何方法内,你都可以通过 BFDamageApi.getContextFor(this) 获取当前上下文。不过大多数管线方法(getArmorLevelgetRHAmodifyPenetrationresolvePenetrationcalculateFinalDamage)已经将 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 调用来自协议管线内部——协议已完成穿甲判定和伤害计算,amountcalculateFinalDamage 的输出。此时你应直接执行伤害,不再走穿甲管线:

@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 的子类——方法签名中使用的是 DamageSourceBFDamageContext,不直接引用 Entity。然而,在实际的 Minecraft 模组开发中,实现 BFHurtTarget 的类几乎总是 LivingEntity 的子类(僵尸、玩家、自定义生物等),因为只有实体才能被 Entity#hurt 拦截。

对于非 Entity 的目标(例如一个逻辑上的装甲块代表、模块化的车辆部件),你可以在 hurt 方法中完全不调用 super.hurt,而是自己维护伤害计数或破坏状态。此时你仍然可以使用协议的穿甲判定管线——getArmorLevelresolvePenetrationcalculateFinalDamage 会正常执行,只是最后的 hurt 由你自定义处理。

还有另一种方式:BFArmorMaterial

如果你的目标是给原版实体(玩家、僵尸等)添加防护——你无法修改它们的类来让它们实现 BFHurtTarget——协议提供了另一条路径:让护甲物品实现 BFArmorMaterial 接口。胸甲、头盔、护腿、靴子都可以通过实现此接口自动为穿戴者提供穿甲判定能力,零事件订阅、零 Manually Mixin。

关于两条路径的对比、BFArmorMaterial 的简易/精密模式、槽位映射、双层串联管线等,见 3.6 BFArmorMaterial 接口