Skip to content

更新类属性(UPDATE_AFTER)

概述

UPDATE_AFTER 在实体的属性数据发生更新后触发。所谓「属性更新」,是指实体的属性面板值发生了变化——比如:

  • 玩家穿上了装备
  • 玩家卸下了装备
  • 通过命令添加/移除了属性源
  • 临时属性源过期了

典型用途:

  • 将面板生命值同步到原版血量(生命值.groovy
  • 将面板移速同步到原版移速(移速加成.groovy
  • 根据面板数据更新实体外观或状态

注意: 非玩家实体也会触发此事件。

需要重写的方法

Groovy
@Override
void onEnable()

/**
 * @param entity  触发者
 * @param handler 处理器
 * @return 属性是否触发
 */
@Override
boolean onUpdateAfter(LivingEntity entity, UpdateAfterHandler handler)
JavaScript
function onEnable(attr)

function onUpdateAfter(attr, entity, handler)

处理器能力

UPDATE_AFTER 的处理器实现了以下接口:

接口能力
AttributeTrackergetRandomValue, getAttrData, trigger
MetadataTrackersetMeta, getMeta, hasMeta

注意: UPDATE_AFTER 没有 DamageTracker,所以你不能在这里调用 handler.addDamage() 等方法。这个类型是用于「同步状态」而不是「制造伤害」的。

第一级:简单的状态同步

一个最简单的 UPDATE_AFTER 属性——在面板更新时发条消息:

Groovy
@CompileStatic
class UpdateNotifier extends GroovyAttribute {

    UpdateNotifier() {
        super(AttributeTypes.UPDATE_AFTER, "update_notify", "更新通知", 1, 0.0D, -1.0D)
    }

    @Override
    boolean onUpdateAfter(LivingEntity entity, UpdateAfterHandler handler) {
        // 仅玩家
        if (entity.getType() != EntityType.PLAYER) return false

        // 读取面板值
        double value = handler.getRandomValue(entity, "更新通知")
        entity.sendMessage("§7[属性更新] 当前值:${String.format("%.2f", value)}")
        return true
    }
}

第二级:同步到原版属性

UPDATE_AFTER 最常见的用法是把面板属性同步到 Minecraft 原版属性系统。以生命值为例:

Groovy
@CompileStatic
class HealthSync extends GroovyAttribute {

    HealthSync() {
        super(AttributeTypes.UPDATE_AFTER, "health_sync", "生命同步", 1, 1.0D, -1.0D)
        // 关闭过滤符检测(生命值通常不需要过滤符)
        this.setSkipFilter(true)
        // 绑定属性源事件:添加/移除属性源时也触发
        this.setMeta("bind source event", true)
    }

    @Override
    boolean onUpdateAfter(LivingEntity entity, UpdateAfterHandler handler) {
        if (entity.getType() != EntityType.PLAYER) return false
        var player = (Player) entity

        // 必须在主线程操作原版属性
        SchedulerUtils.sync {
            // 获取原版最大生命值属性实例
            AttributeInstance attrInstance = entity.getAttribute(BukkitAttribute.MAX_HEALTH())

            // 获取面板生命值
            double value = handler.getRandomValue(entity, "生命同步")

            // 创建属性修饰符
            AttributeModifier modifier = BukkitUtils.createAttributeModifier(
                "CraneAttribute 生命同步", value
            )

            // 移除旧修饰符(防止叠加)然后添加新的
            double lastMax = attrInstance.getValue()
            attrInstance.removeModifier(modifier)
            attrInstance.addModifier(modifier)
            double newMax = attrInstance.getValue()

            // 修正溢出:如果当前血量超过新上限,削减到上限
            if (lastMax != newMax) {
                if (entity.getHealth() > newMax) {
                    entity.setHealth(newMax)
                }
                // 预防假死(生命压缩模式下的特殊情况)
                if (player.isHealthScaled()) {
                    double scale = player.getHealthScale()
                    if (lastMax < newMax * 1 / scale) {
                        entity.setHealth(newMax * 1 / scale)
                    }
                }
            }
        }
        return true
    }
}

逐行理解:

  1. SchedulerUtils.sync { ... } — 操作原版 Bukkit API 必须在主线程执行。onUpdateAfter 可能在后台线程被调用,所以用 SchedulerUtils.sync 调度到主线程。

  2. BukkitAttribute.MAX_HEALTH() — 获取原版「最大生命值」属性的 Attribute 枚举。高版本 Minecraft 中,玩家属性需要用 Attribute 而非字符串来获取。

  3. AttributeModifier — 原版属性修改器。我们创建它来给玩家的最大生命值增加我们面板上的数值。

  4. removeModifier + addModifier — 先移除旧的再添加新的,防止数值叠加。因为 onUpdateAfter 可能被多次调用(每次属性源变动都触发),如果不移除旧的,数值会一直累加。

  5. 血量溢出修正 — 如果玩家原本有 100 血,现在上限降到了 80,那么当前血量也应该降到 80。

第三级:移速加成

Groovy
@CompileStatic
class SpeedSync extends GroovyAttribute {

    SpeedSync() {
        super(AttributeTypes.UPDATE_AFTER, "speed_sync", "移速同步", 1, 0.0D, -1.0D)
        this.setSkipFilter(true)
        this.setMeta("bind source event", true)
    }

    @Override
    boolean onUpdateAfter(LivingEntity entity, UpdateAfterHandler handler) {
        if (entity.getType() != EntityType.PLAYER) return false

        SchedulerUtils.sync {
            AttributeInstance attrInstance = entity.getAttribute(BukkitAttribute.MOVEMENT_SPEED())
            double value = handler.getRandomValue(entity, "移速同步")

            // 移速是乘算的!不能直接加绝对值,否则会快到飞起
            // 基础移速约 0.1,我们把面板值作为百分比加成
            double bonusSpeed = value / 100 * attrInstance.getBaseValue()

            AttributeModifier modifier = BukkitUtils.createAttributeModifier(
                "CraneAttribute 移速同步", bonusSpeed
            )
            attrInstance.removeModifier(modifier)
            attrInstance.addModifier(modifier)
        }
        return true
    }
}

关键点: 原版移速是乘算的,基础值约 0.1。如果你直接 addModifier(AttributeModifier("name", value, ADD_NUMBER)),那么即使 value=1,玩家也会快到无法控制。正确的做法是:面板值作为百分比 × 基础值 = 实际加值。

bind source event 元数据

在构造函数中调用 this.setMeta("bind source event", true) 是一个重要的设置。

不加这个设置时: onUpdateAfter 只在属性被其他方式(如切换装备)触发更新时调用。

加了这个设置后: 当一个包含此属性的属性源被添加或移除时,也会触发 onUpdateAfter

JavaScript 完整示例

JavaScript
var type = "UPDATE_AFTER"
var id = "my_health"
var name = "我的生命值"
var priority = 1
var power = 1.0
var max = -1.0

function onEnable(attr) {
    // 绑定属性源事件——添加/移除装备时也触发
    attr.setMeta("bind source event", true)
    // 跳过过滤符检测
    attr.setSkipFilter(true)
}

function onUpdateAfter(attr, entity, handler) {
    if (entity.getType() != org.bukkit.entity.EntityType.PLAYER) return false

    var value = handler.getRandomValue(entity, "我的生命值")
    entity.sendMessage("当前生命值加成: " + value.toFixed(2))
    return true
}

常见问题

Q: 为什么穿装备后血量没变化? 检查是否设置了 this.setMeta("bind source event", true)。没有这个设置的话,只有属性更新时才会触发更新。

Q: 为什么血量一直增加? 检查是否在添加新的 AttributeModifier 之前移除了旧的。没有移除的话,每次更新都会叠加一个修饰符。

Q: UPDATE_AFTER 和 RUNTIME_AFTER 有什么区别?

  • UPDATE_AFTER事件驱动——属性变了就触发
  • RUNTIME_AFTER时间驱动——每隔 N tick 触发一次

简单说:需要「即时响应属性变化」用 UPDATE_AFTER;需要「周期性自动执行」用 RUNTIME_AFTER。