3.4 最终伤害计算¶
协议的第三和第四步——resolvePenetration 和 calculateFinalDamage——共同决定了"打不打得穿"和"打穿了有多少伤害"。这两个步骤紧密相连:resolvePenetration 的返回值是 calculateFinalDamage 的输入。本节详解两个方法的默认逻辑、覆写时机和常见覆写模式。
resolvePenetration:穿甲判定¶
resolvePenetration 的返回值是 PenetrationResult 枚举——PENETRATED、BLOCKED、RICOCHET 三选一,互斥。这个返回值是整个管线的分水岭:它决定了后续 calculateFinalDamage 收到的 result 参数,也决定了武器侧 BFDamageHandler 中哪些回调会被触发。
默认实现非常简洁——直接委托 isArmorPenetrated,仅区分击穿和未击穿,不判定跳弹:
default PenetrationResult resolvePenetration(BFDamageContext ctx) {
return isArmorPenetrated(ctx) ? PenetrationResult.PENETRATED : PenetrationResult.BLOCKED;
}
default boolean isArmorPenetrated(BFDamageContext ctx) {
return ctx.getPenetrationLevel().canDefeat(getArmorLevel(ctx));
}
这意味着在简易模式下,一次命中要么击穿要么未击穿,不存在跳弹。如果你需要跳弹判定——当弹丸入射角过大时偏转飞走——就应该覆写 resolvePenetration。
引入跳弹判定¶
跳弹的发生条件是入射角极大——弹丸几乎擦着装甲表面飞过,没有足够的法向分量来"咬住"表面。覆写 resolvePenetration 的标准模式是:先根据角度判断是否跳弹,再根据穿深判定是否击穿。
@Override
public PenetrationResult resolvePenetration(BFDamageContext ctx) {
// 计算入射角
Vec3 shotDir = ctx.hitVelocity().normalize();
double cosTheta = Math.abs(shotDir.dot(ctx.hitNormal()));
double angleDeg = Math.toDegrees(Math.acos(cosTheta));
// 入射角 > 70° 判定为跳弹
if (angleDeg > 70.0) {
return PenetrationResult.RICOCHET;
}
// 正常穿甲判定
float rha = getRHA(ctx);
float effectivePen = modifyPenetration(ctx);
if (effectivePen > rha) {
return PenetrationResult.PENETRATED;
}
return PenetrationResult.BLOCKED;
}
跳弹阈值的设定取决于装甲类型和弹种。倾斜装甲(如 T-34 的前装甲 60° 倾角)天然更容易引发跳弹。你可以根据扩展字段中的弹种信息或口径来调整阈值——全口径 AP 弹比 APFSDS 长杆弹更容易跳弹,设定更低的跳弹角度阈值即可。
在一个覆写了 resolvePenetration 的精密模式中,通常不会再依赖 isArmorPenetrated。直接使用 modifyPenetration(ctx) > getRHA(ctx) 做精确 float 比较即可——如果覆写了 getRHA,比较的是精确等效厚度;如果覆写了 modifyPenetration,比较的是修正后的穿深。
calculateFinalDamage:默认三级模型¶
calculateFinalDamage 接收 PenetrationResult 作为参数,返回 float 最终伤害量。默认的三级模型基于离散等级比较:
default float calculateFinalDamage(BFDamageContext ctx, PenetrationResult result) {
if (result != PenetrationResult.PENETRATED) return 0f;
if (ctx.getPenetrationLevel() == getArmorLevel(ctx)) return ctx.baseDamage() * 0.65f;
return ctx.baseDamage();
}
这个模型表达的设计意图是:刚好打穿(同等级)时弹药已消耗过半,伤害仅剩 65%;越级击穿时弹药几乎完整穿透,伤害不打折;未击穿或跳弹则完全没有伤害。
三级模型的 65% 折扣是一个游戏性设计参数,而非物理模拟。它的作用是创造"弹种升级"的激励机制——刚好能打穿还不够,你需要越级击穿才能发挥全部伤害。如果你的游戏不需要这种梯度,完全可以在覆写中返回不同的倍率,或基于实际的 penetration - getRHA 差值来计算伤害比例。
钝伤:未击穿也有伤害¶
在默认模型中,未击穿和跳弹的伤害为 0。但对于大口径弹药——即使没有穿透装甲,冲击波和装甲背板的崩落(spall)仍可能对内部乘员或结构造成伤害。覆写 calculateFinalDamage 可以引入非零钝伤:
@Override
public float calculateFinalDamage(BFDamageContext ctx, PenetrationResult result) {
float baseDamage = ctx.baseDamage();
switch (result) {
case PENETRATED:
// 穿透:根据剩余穿深决定伤害倍率
float effectivePen = modifyPenetration(ctx);
float rha = getRHA(ctx);
float overmatchRatio = effectivePen / Math.max(rha, 1f);
if (overmatchRatio > 2.0f) {
return baseDamage * 1.5f; // 超匹配:碾压穿透,伤害加成
}
// 剩余穿深越多,伤害越完整
float residualRatio = (effectivePen - rha) / Math.max(rha, 1f);
return baseDamage * Math.clamp(residualRatio, 0.3f, 1.0f);
case BLOCKED:
// 未击穿:大口径弹药仍有钝伤
float caliber = ctx.extensions().get(BFDamageExtensions.CALIBER);
if (caliber > 0.075f) {
// 大口径(>75mm)未击穿仍有 20% 钝伤
return baseDamage * 0.2f;
}
return 0f;
case RICOCHET:
// 跳弹:弹丸擦过表面,仅产生少量钝伤
float mass = ctx.extensions().get(BFDamageExtensions.MASS);
return baseDamage * 0.05f * (mass / 10f); // 质量越大,跳弹钝伤越明显
default:
return 0f;
}
}
这个覆写展示了 calculateFinalDamage 的完整设计空间——你可以根据穿甲结果、剩余穿深、弹药属性(口径、质量、弹种)来做完全自由的计算。协议不限制你的伤害模型,它只负责在正确的时间将正确的 result 传递给你。
超匹配加成与破片伤害¶
BFDamageHandler 中定义的超匹配(Overmatch)概念——穿深远大于装甲厚度——在 calculateFinalDamage 中也可以体现为伤害加成。默认的 isOvermatch 阈值是 1.5 倍,覆写 calculateFinalDamage 时可以读取相同的条件来追加伤害:
if (result == PenetrationResult.PENETRATED) {
float effectivePen = modifyPenetration(ctx);
float rha = getRHA(ctx);
float ratio = effectivePen / Math.max(rha, 1f);
if (ratio > 1.5f) {
return baseDamage * 1.3f; // 超匹配:30% 伤害加成
}
}
破片伤害(内部崩落)是另一种常见的奖励机制。弹丸在穿透过程中碎裂会产生破片,对内部造成额外伤害。如果武器侧的 BFDamageHandler.isSpall 返回了 true,说明本次穿透产生了破片。虽然 calculateFinalDamage 不能直接调用 isSpall(它需要 handler 引用),但你可以通过上下文判断——例如检查口径是否小于某个值(大口径更容易产生大破片):
float mass = ctx.extensions().get(BFDamageExtensions.MASS);
if (mass > 5f) {
// 大质量弹丸穿透产生破片,额外伤害
bonusDamage += mass * 0.5f;
}
与 hurt 方法的协作¶
calculateFinalDamage 返回的伤害值最终交给 hurt(DamageSource, float) 执行。两者之间的一个重要隐含契约是:hurt 方法不应再次修改伤害量——它应该直接使用传入的 amount。如果需要在 hurt 中做额外修正(如原版护甲减免),应该意识到这是叠加在协议伤害之上的第二层修正。
一个更安全的实践是:所有协议层面的伤害计算都在 calculateFinalDamage 中完成,hurt 仅负责执行。如果 hurt 调用了 super.hurt(source, amount),原版的护甲机制会再次削减伤害——这恰好模拟了"穿透后仍有残余防护"的效果。如果你不想原版护甲再削减,可以直接操作 health 值而不调用 super.hurt。
无论哪种处理方式,hurt 的返回值(boolean)会影响 BFDamageApi.hurt() 的返回值。如果 hurt 返回 false,BFDamageApi.hurt() 返回 0——表明伤害被免疫或拦截了。武器侧可以利用这一点区分"打穿但被免疫"和"根本没打穿"。