跳转至

3.6 BFArmorMaterial 接口

BFArmorMaterial 是协议为护甲物品提供的核心接口。它的设计目标是让无法修改实体类的护甲模组也能接入穿甲管线——让胸甲、头盔、护腿等物品类实现此接口,穿戴该物品的实体便自动获得穿甲判定能力,无需任何 Mixin、事件订阅或手动拦截。

与 BFHurtTarget 的对比

维度 BFHurtTarget BFArmorMaterial
实现者 实体类(自定义生物、载具) 物品类(头盔、胸甲、护腿、靴子)
防护粒度 命中部位(一次命中走整条管线) 装备槽位(一次命中只命中一个槽位)
管线方法签名 xxx(BFDamageContext ctx) xxx(EquipmentSlot slot, BFDamageContext ctx)
原版伤害兼容 createContextFromVanilla 可返回 null 退回原版 createContextFromVanilla 默认拦截所有,可覆写为按类型放行
典型场景 装甲僵尸、坦克实体、魔法傀儡 防弹胸甲、防爆头盔、外骨骼护腿

两条路径的简易/精密双模式设计完全对称——在 3.1\~3.5 各节中讲解的 BFHurtTarget 覆写策略,几乎都可以直接翻译到 BFArmorMaterial 的逐槽位方法上。本节专注于两者之间的差异点,建议先阅读 3.1\~3.4 建立完整的管线概念后再回到本节。

简易模式:声明槽位护甲等级

getArmorLevel(slot, ctx)

简易模式的核心方法。根据装备槽位和命中上下文返回离散护甲等级。协议适配器在调用此方法前已确定了命中了哪个槽位。

@Override
public ArmorLevel getArmorLevel(EquipmentSlot slot, @Nullable BFDamageContext ctx) {
    switch (slot) {
        case HEAD:
            return ArmorLevel.MEDIUM;       // 头盔:10~20mm(NIJ IIIA 级别)
        case CHEST:
            return ArmorLevel.SUPER_HEAVY_2; // 胸甲:80~150mm(含陶瓷插板)
        case LEGS:
            return ArmorLevel.LIGHT_2;       // 护腿:5~10mm
        case FEET:
            return ArmorLevel.UNARMORED_1;   // 靴子:不提供额外防护
        default:
            return ArmorLevel.UNARMORED_1;
    }
}

ctx 参数可以为 null——当适配器在兜底加权平均模式下查询护甲信息时(例如爆炸等无命中点伤害调用 resolveBestSlot,遍历各槽位时传入 null)。你在覆写时需要处理这种情况:

@Override
public ArmorLevel getArmorLevel(EquipmentSlot slot, @Nullable BFDamageContext ctx) {
    if (ctx != null && ctx.getHandler() instanceof ChemicalEnergyWarhead) {
        // 爆反对化学能弹头有效——等效防护提升一级
        if (slot == EquipmentSlot.CHEST) {
            return ArmorLevel.SUPER_HEAVY_3;
        }
    }
    return baseLevelForSlot(slot);
}

mapHitToSlot:命中点 → 装备槽位

默认实现按命中点高度占比将一次命中映射到对应槽位:头部(>85%身高)、躯干(55%\~85%)、腿部(35%\~55%)、脚部(<35%)。覆写此方法可实现自定义映射——例如全覆盖头盔将部分躯干高度也纳入头部槽位:

@Override
@Nullable
public EquipmentSlot mapHitToSlot(LivingEntity wearer, BFDamageContext ctx) {
    Vec3 hitPoint = ctx.hitPoint();
    if (hitPoint == null || hitPoint.equals(Vec3.ZERO)) return null;

    Vec3 localHit = hitPoint.subtract(wearer.position());
    double heightFrac = localHit.y / wearer.getBbHeight();

    // 全覆盖头盔:所有头部以上、以及面部正前方的命中都归入 HEAD
    if (heightFrac > 0.8 || isFrontalFaceHit(wearer, hitPoint)) {
        return EquipmentSlot.HEAD;
    }
    // 其余用默认映射
    return super.mapHitToSlot(wearer, ctx);
}

每个槽位的护甲物品各自覆写 mapHitToSlot。适配器遍历四个槽位,第一件返回当前槽位的物品胜出。这意味着你可以让一件"全覆盖头盔"通过返回 HEAD 来抢在胸甲之前处理本应由胸甲处理的躯干上部命中——只要你认为这部分区域属于头盔的防护范围。

createContextFromVanilla:控制原版伤害接管

护甲物品的 createContextFromVanilla(DamageSource, float) 决定了哪些原版伤害会被转换为协议伤害。默认实现始终返回有效上下文(穿深 = 原版伤害量 / 2),意味着所有原版伤害默认进入穿甲管线

如果你需要让某些伤害类型绕过护甲——魔法、虚空、溺水等不应被钢板阻挡——覆写此方法返回 null

@Override
@Nullable
public BFDamageContext createContextFromVanilla(DamageSource source, float amount) {
    // 魔法、虚空、溺水——这些不应该被钢板阻挡,放行原版
    if (source.is(DamageTypes.MAGIC)
        || source.is(DamageTypes.OUT_OF_WORLD)
        || source.is(DamageTypes.DROWN)) {
        return null;
    }
    // 物理伤害走穿甲判定
    return BFDamageContext.builder()
        .source(source)
        .baseDamage(amount)
        .penetration(amount / 2f)
        .build();
}

适配器在 Mixin 拦截阶段遍历所有护甲槽位,调用每件物品的此方法。任一护甲物品返回非 null 时整次伤害被接管;所有护甲物品均返回 null 时才退回原版流程。这意味着如果你同时穿戴了拦截物理伤害的胸甲(返回 ctx)和拦截爆炸伤害的头盔(返回 ctx),僵尸攻击和 TNT 爆炸都会被接管——两者的过滤条件产生"并集"效果。

BFHurtTarget.createContextFromVanilla 的差异在于:实体的版本默认返回 null(不接管),护甲物品的版本默认返回 ctx(全部接管)。这是因为两者的定位不同——护甲物品的存在本身就是"我需要防护"的声明。


精密模式:逐槽位管线覆写

BFArmorMaterial 的精密模式方法与 BFHurtTarget 的对应方法在逻辑上完全相同——差异仅在于每个方法多了一个 EquipmentSlot slot 参数。下面逐一展示覆写示例。

getRHA(slot, ctx):精确等效厚度

@Override
public float getRHA(EquipmentSlot slot, @Nullable BFDamageContext ctx) {
    if (slot == EquipmentSlot.CHEST) {
        // 胸甲:陶瓷插板 120mm + 芳纶背板 15mm = 135mm RHA
        return 135f;
    }
    return getArmorLevel(slot, ctx).medianRha();
}

覆写了 getRHA 后,建议在 getArmorLevel 中也调用 ArmorLevel.fromRha(getRHA(slot, ctx)),让等级始终与精确 RHA 保持一致——默认的 calculateFinalDamage 依赖等级做同级/越级区分,HUD 也使用等级来显示。

modifyPenetration(slot, ctx):槽位减效

@Override
public float modifyPenetration(EquipmentSlot slot, BFDamageContext ctx) {
    float pen = ctx.penetration();

    if (slot == EquipmentSlot.CHEST) {
        // 胸甲搭载了爆炸反应装甲(ERA)——仅对化学能弹头有效
        BFDamageHandler handler = ctx.getHandler();
        if (handler instanceof ChemicalEnergyWarhead) {
            pen -= 150f;  // 爆反对化学能射流削减 150mm 穿深
        }
    }

    // 间隙衰减:穿深随命中距离递减(多层装甲)
    // 详见 3.3 节的完整讨论——此处逻辑与 BFHurtTarget 完全相同
    return Math.max(0, pen);
}

BFHurtTarget.modifyPenetration(ctx) 的唯一区别在于:你现在可以在不同槽位上做不同的减效决策——胸甲有爆反而头盔没有,腹部有间隙装甲而四肢没有。槽位粒度让减效逻辑更加精细。

resolvePenetration(slot, ctx):含跳弹的穿甲判定

@Override
public PenetrationResult resolvePenetration(EquipmentSlot slot, BFDamageContext ctx) {
    float rha = getRHA(slot, ctx);
    float pen = modifyPenetration(slot, ctx);

    // 高倾角装甲板——入射角 > 65° 判定为跳弹
    Vec3 shotDir = ctx.hitVelocity().normalize();
    double cosTheta = Math.abs(shotDir.dot(ctx.hitNormal()));
    double angleDeg = Math.toDegrees(Math.acos(cosTheta));
    if (angleDeg > 65.0) {
        return PenetrationResult.RICOCHET;
    }

    return pen >= rha
        ? PenetrationResult.PENETRATED
        : PenetrationResult.BLOCKED;
}

跳弹判定是 resolvePenetration 覆写的最常见原因。不同槽位的护甲可以有不同的跳弹阈值——头盔通常更圆滑(跳弹阈值 60°),胸甲更平坦(跳弹阈值 70° 以上)。

isArmorPenetrated(slot, ctx):纯击穿判定

默认实现基于离散等级比较——ArmorLevel.fromRha(modifyPenetration).canDefeat(getArmorLevel)。通常不需要单独覆写,因为覆写了 resolvePenetrationisArmorPenetrated 不再被调用。如果只想在默认判定中微调等级阈值(例如让同级也算未击穿),可以覆写此方法。

calculateFinalDamage(slot, ctx, result):钝伤与细粒度伤害

默认三级模型在 3.4 节已详细讨论。BFArmorMaterial 版本的差异在于可以按槽位设定不同的伤害曲线——头部命中可能造成额外伤害加成,腿部命中可能仅致残而不致死:

@Override
public float calculateFinalDamage(EquipmentSlot slot, BFDamageContext ctx,
                                   PenetrationResult result) {
    float baseDamage = ctx.baseDamage();

    switch (result) {
        case PENETRATED:
            float factor = switch (slot) {
                case HEAD -> 2.0f;    // 头部穿透:双倍伤害
                case CHEST -> 1.0f;   // 躯干穿透:正常伤害
                case LEGS, FEET -> 0.5f; // 四肢穿透:半额伤害
                default -> 1.0f;
            };
            return baseDamage * factor;

        case BLOCKED:
            // 大口径弹药即使未击穿也有钝伤——冲击波透过胸甲传递
            float caliber = ctx.extensions().get(BFDamageExtensions.CALIBER);
            if (caliber > 0.075f && slot == EquipmentSlot.CHEST) {
                return baseDamage * 0.15f;  // 15% 钝伤
            }
            return 0f;

        case RICOCHET:
            return 0f;  // 跳弹无伤害
        default:
            return 0f;
    }
}

完整精密模式示例:ERA 爆反胸甲

下面是一个完整的 BFArmorMaterial 精密模式实现——将 3.3 节和 3.4 节中 BFHurtTarget 的所有覆写翻译到了槽位维度。它包含:胸甲有爆反拦截(根据弹种判断)、头盔引入跳弹判定(圆滑外形更易跳弹)、躯干计算钝伤(大口径冲击波)。

public class ERAHeavyChestplate extends ChestplateItem implements BFArmorMaterial {

    public ERAHeavyChestplate(Properties properties) {
        super(ArmorMaterials.IRON, Type.CHESTPLATE, properties);
    }

    // ===== 基础防护 =====

    @Override
    public ArmorLevel getArmorLevel(EquipmentSlot slot, @Nullable BFDamageContext ctx) {
        return switch (slot) {
            case CHEST -> ArmorLevel.SUPER_HEAVY_4;  // 300~600mm
            default -> ArmorLevel.UNARMORED_1;
        };
    }

    @Override
    public float getRHA(EquipmentSlot slot, @Nullable BFDamageContext ctx) {
        if (slot == EquipmentSlot.CHEST) {
            return 350f;  // 基甲 350mm RHA(不含爆反)
        }
        return 0f;
    }

    // ===== 爆反拦截 =====

    @Override
    public float modifyPenetration(EquipmentSlot slot, BFDamageContext ctx) {
        float pen = ctx.penetration();
        if (slot != EquipmentSlot.CHEST) return pen;

        BFDamageHandler handler = ctx.getHandler();
        if (handler instanceof ChemicalEnergyWarhead) {
            pen -= 150f;  // ERA 对化学能射流削减 150mm
        } else {
            pen -= 30f;   // 对动能弹药仅轻微衰减
        }
        return Math.max(0, pen);
    }

    // ===== 跳弹与穿甲判定 =====

    @Override
    public PenetrationResult resolvePenetration(EquipmentSlot slot, BFDamageContext ctx) {
        float rha = getRHA(slot, ctx);
        float pen = modifyPenetration(slot, ctx);

        Vec3 shotDir = ctx.hitVelocity().normalize();
        double cosTheta = Math.abs(shotDir.dot(ctx.hitNormal()));
        double angleDeg = Math.toDegrees(Math.acos(cosTheta));

        // 平坦胸甲:>75° 才跳弹
        if (angleDeg > 75.0) {
            return PenetrationResult.RICOCHET;
        }

        return pen >= rha
            ? PenetrationResult.PENETRATED
            : PenetrationResult.BLOCKED;
    }

    // ===== 伤害计算 =====

    @Override
    public float calculateFinalDamage(EquipmentSlot slot, BFDamageContext ctx,
                                       PenetrationResult result) {
        return switch (result) {
            case RICOCHET -> ctx.baseDamage() * 0.05f;   // 跳弹仍有 5% 钝伤
            case BLOCKED -> ctx.baseDamage() * 0.15f;    // 未击穿 15% 钝伤
            case PENETRATED -> ctx.baseDamage();          // 击穿全伤害
        };
    }

    // ===== 槽位映射(默认按高度划分,无需覆写) =====
}

两层防护模型

BFArmorMaterialhurt(由适配器提供)直接委托 entity.hurt(source, amount) 走原版伤害管线。这意味着即使协议判定为击穿、calculateFinalDamage 返回了 100 点伤害,原版的护甲属性(ARMOR / ARMOR_TOUGHNESS)和保护附魔(Protection 附魔)仍会在此之上做二次减免。

这不是 bug,而是刻意设计的两层防护模型:

处理什么 由谁提供
第一层:协议穿甲 弹头能否穿透护甲。决定是否击穿、残余伤害量 BFArmorMaterial(你的代码)
第二层:原版减免 穿透后的伤害数值减免。ARMOR/TOUGHNESS/Protection 附魔 原版护甲系统

例如,防弹胸甲判定为击穿,协议计算最终伤害 20 HP。但原版钻石甲的 80% 减伤将其削减为 4 HP——相当于弹头穿透了防弹插板,但仍被背板(玩家默认护甲值所代表的内衬)部分阻挡。这是符合物理直觉的——外部护甲挡不住所有伤害,内部的二次防护仍然工作。

如果你希望自己的护甲物品完全替代原版护甲值(而非叠加),可以在物品类中覆写原版的 getDefense() 返回 0,让原版护甲减免为零。协议层不干预这一决策。


与 BFHurtTarget 同时生效时的行为

当一个实体自己实现了 BFHurtTarget,且身上穿戴了 BFArmorMaterial 护甲时,协议按双层串联管线处理:

武器命中实体
第一层:BFArmorMaterial 护甲拦截
  ├─ modifyPenetration(slot, ctx)    → 爆反拦截、间隙衰减
  ├─ resolvePenetration(slot, ctx)   → PENETRATED/BLOCKED/RICOCHET
  └─ calculateFinalDamage(slot, ctx) → 残余伤害
  childCtx = ctx.childContext(residualDmg, PENETRATED ? residualPen : 0f)
第二层:BFHurtTarget 本体判定
  ├─ resolvePenetration(childCtx)         → 基于残余穿深的判定
  ├─ calculateFinalDamage(childCtx, r)    → 含钝伤的最终伤害
  └─ hurt(source, finalDmg)               → 实际扣血

无论第一层护甲是否击穿,第二层本体始终执行完整管线。差别仅在 childCtx 的穿深——击穿时为残余穿深(爆反减效后的值),未击穿/跳弹时为 0(表示没有弹头穿透护甲,仅有钝伤)。

例如,一辆载具(实现 BFHurtTarget 的实体)搭载了 ERA 爆反胸甲,被 HEAT 弹命中:

  1. 第一层:胸甲的 modifyPenetration 从 200mm 削减到 50mm;resolvePenetration 判定 50mm < 基甲 350mm → BLOCKED;calculateFinalDamage 返回 15% 钝伤 = 5.25 HP。
  2. 第二层childCtx 穿深 = 0,baseDamage = 5.25。载具本体 resolvePenetration(穿深=0)fromRha(0) = UNARMORED_1.canDefeat(本体UNARMORED_1) → true → PENETRATED。calculateFinalDamage: 同级击穿 → 65% = 3.41 HP。hurt(3.41) → 实际扣血。

结果是:HEAT 弹未能击穿 ERA + 基甲的组合,但炸药装药的钝伤透过层层防护最终对车内乘员造成了约 3.4 点伤害。这个数值可能看起来小,但它是双层判定的叠加效应——护甲层已将 35 点原始伤害削减到 5.25,本体层再削到 3.41。


武器侧回调中的 BFArmorMaterial

武器侧在 BFDamageHandler 回调中收到的 BFHurtTarget target 参数,在护甲物品路径下是 BFArmorAdapter 实例。通过 target.getBFEntity() 可安全获取被包裹的实体:

@Override
public void onPenetrated(BFHurtTarget target, BFDamageContext ctx) {
    Entity hitEntity = target.getBFEntity();
    if (hitEntity instanceof Player player) {
        // 播放命中音效、生成粒子
        player.playSound(myPenetrationSound, 1.0f, 1.0f);
    }
}

对于直接实现 BFHurtTarget 的实体,getBFEntity() 返回 this——因此武器侧不需要区分目标类型。

如果同时满足两条路径(BFHurtTarget + BFArmorMaterial),回调在第二层(本体层)的结果上触发——武器侧拿到的是本体的最终判定结果,而非护甲层的中间结果。

下一步

你已经掌握了 BFArmorMaterial 的完整开发指南。关于管线内部的适配器如何工作、双层串联的实现细节,可参考穿甲判定管线,其中详细说明了 BFDamageApi.hurt() 的三个分支(复合目标 / BFHurtTarget / BFArmorMaterial 适配器)。协议外伤害的 Mixin 拦截流程见ThreadLocal 与 Mixin