跳转至

1.3 武器模组:发起协议伤害

武器侧开发者的职责是构造命中的完整上下文,然后调用协议入口。协议层接管后续的穿甲判定、伤害计算、伤害执行和回调触发。本节展示从零开始的最简流程,后续章节将逐一展开每个步骤的细节。

最简示例

以下是最简的武器侧代码——当一颗子弹命中某个实体时,构造上下文并发起协议伤害。

// 假设你已经从 raycast 中获取了命中结果
EntityHitResult hitResult = ...;       // 射线命中的结果
Vec3 bulletVelocity = ...;             // 弹丸速度矢量(m/s)
float bulletPenetration = 120f;        // 弹丸理论穿深(mm RHA),已由武器侧自行计算角度修正
DamageSource damageSource = ...;       // 原版 DamageSource,指定伤害类型和来源

BFDamageContext ctx = BFDamageContext.builder()
    .source(damageSource)
    .baseDamage(35f)
    .hitVelocity(bulletVelocity)
    .hitPoint(hitResult.getLocation())
    .hitNormal(new Vec3(hitResult.getDirection().getNormal().getX(),
                        hitResult.getDirection().getNormal().getY(),
                        hitResult.getDirection().getNormal().getZ()))
    .penetration(bulletPenetration)
    .build();

float dealt = BFDamageApi.hurt(hitResult.getEntity(), ctx);

这就是发起一次协议伤害所需的全部代码。BFDamageApi.hurt() 返回实际造成的伤害量——如果目标是普通实体(未实现 BFHurtTarget),协议直接走原版 entity.hurt(source, baseDamage) 并返回结果;如果目标实现了 BFHurtTarget,则走完整的穿甲判定管线。

Builder 的默认值

BFDamageContext.builder() 只要求 source 必须设置,其他字段都有安全的默认值。hitVelocityhitPointhitNormal 默认均为 Vec3.ZERObaseDamage 默认 0,penetration 默认 0,extensions 默认一个空容器。因此即使你只设置了 sourcebaseDamage,也能得到一个合法的上下文——尽管这样的上下文缺少弹道信息,目标实体在判定时只能依赖于离散的 ArmorLevel 等级比较。

注册全局扩展 key

如果你的模组需要在命中上下文中携带自定义的结构化数据(例如弹药类型、炸药装药量等),可以使用类型安全的扩展机制。扩展 key 建议在模组初始化时注册,并存为 public static final 常量:

// 在你的 @Mod 类或 config 类中
public static final BFDamageExtensionKey<Float> EXPLOSIVE_MASS =
    BFDamageExtensions.register(
        ResourceLocation.fromNamespaceAndPath("your_mod_id", "explosive_mass"),
        Float.class,
        () -> 0f    // 默认值工厂——未设置时返回此值
    );

构造上下文时,通过 set() 写入扩展值:

exts.set(YourMod.EXPLOSIVE_MASS, 2.5f);

护甲侧在管线方法内通过 ctx.extensions().get(YourMod.EXPLOSIVE_MASS) 即可读取——如果武器侧未设置,将退回注册时指定的默认值。协议自身已预定义了三个标准 key:BFDamageExtensions.FUSE_DELAY(引信延迟)、BFDamageExtensions.CALIBER(口径)和 BFDamageExtensions.MASS(弹体质量),可以直接使用而不必额外注册。

添加回调处理

武器侧往往需要在伤害完成后执行后续效果——击穿时播放火花粒子、未击穿时播放跳弹音效、产生破片时生成二次弹丸。通过实现 BFDamageHandler 接口并注入上下文,可以在管线的末尾接收到这些事件:

BFDamageContext ctx = BFDamageContext.builder()
    .source(damageSource)
    .baseDamage(35f)
    .hitVelocity(bulletVelocity)
    .hitPoint(hitResult.getLocation())
    .hitNormal(hitNormal)
    .penetration(bulletPenetration)
    .handler(myHandler)         // 注入 handler
    .build();

float dealt = BFDamageApi.hurt(hitResult.getEntity(), ctx);

BFDamageHandler 提供了两个层面的钩子。事件回调层面,有 onPenetratedonBlockedonRicochetonOvermatchonSpall 五个方法,均为 default 空实现,按需覆写即可。前三个主结果回调由穿甲结果决定;onOvermatchonSpall 则只由 isOvermatch / isSpall 的返回值决定。默认判定逻辑中,超匹配定义为"穿深 > 装甲厚度 × 1.5",破片定义为"未击穿"或"击穿但非超匹配"。

此外,BFDamageHandler 还提供了便捷方法 dealDamage(target, ctx),它会自动将自身注入上下文后再调用 BFDamageApi.hurt(),使调用方不必手动调用 withHandler

// 等价于 BFDamageApi.hurt(target, ctx.withHandler(this))
float dealt = myHandler.dealDamage(target, ctx);

角度修正由武器侧负责

一个重要的约定是:穿深的角度效应应由武器侧在构造上下文前自行计算。协议不默认进行斜穿修正——modifyPenetration 方法的默认实现直接返回原始穿深,它存在的意义是供护甲侧实现爆反拦截、间隙衰减等额外的减效逻辑。

因此在调用协议之前,武器侧应该根据入射角对理论穿深做修正。例如,如果弹丸速度矢量与命中面法线反方向之间的夹角为 θ,则等效穿深近似为 penetration / cosθ。将修正后的值填入 penetration 字段即可。

下一步

你已经掌握了武器侧发起协议伤害的基本流程。下一节将从护甲侧的角度,展示如何让一个实体响应协议伤害。如果你希望深入了解上下文的构造细节——每个字段的含义、命中几何信息的计算方式、扩展注册的生命周期——可以跳转到第二章。