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)。通常不需要单独覆写,因为覆写了 resolvePenetration 后 isArmorPenetrated 不再被调用。如果只想在默认判定中微调等级阈值(例如让同级也算未击穿),可以覆写此方法。
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(); // 击穿全伤害
};
}
// ===== 槽位映射(默认按高度划分,无需覆写) =====
}
两层防护模型¶
BFArmorMaterial 的 hurt(由适配器提供)直接委托 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 弹命中:
- 第一层:胸甲的
modifyPenetration从 200mm 削减到 50mm;resolvePenetration判定 50mm < 基甲 350mm → BLOCKED;calculateFinalDamage返回 15% 钝伤 = 5.25 HP。 - 第二层:
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。