附魔

附魔

附魔是可应用到工具等物品上的特殊效果。从 1.21 起,附魔以数据组件存储在物品上,由 JSON 定义,并由附魔效果组件构成。游戏中,特定物品的附魔位于 DataComponentTypes.ENCHANTMENT 组件中的 ItemEnchantment 实例。

在自己命名空间的 enchantment 数据包子目录创建 JSON,即可添加附魔。例如,原文给出的 examplemod:example_enchant 示例文件为 data/examplemod/enchantment/example_enchantment.json。

附魔 JSON 格式

{
    // The text component that will be used as the in-game name of the enchantment.
    // Can be a translation key or a literal string. 
    // Remember to translate this in your lang file if you use a translation key!
    "description": {
        "translate": "enchantment.examplemod.enchant_name"
    },
    
    // Which items this enchantment can be applied to.
    // Can be either an item id, such as "minecraft:trident",
    // or a list of item ids, such as ["examplemod:red_sword", "examplemod:blue_sword"]
    // or an item tag, such as "#examplemod:enchantable/enchant_name".
    // Note that this doesn't cause the enchantment to appear for these items in the enchanting table.
    "supported_items": "#examplemod:enchantable/enchant_name",

    // (Optional) Which items this enchantment appears for in the enchanting table.
    // Can be an item, list of items, or item tag.
    // If left unspecified, this is the same as `supported_items`.
    "primary_items": [
        "examplemod:item_a",
        "examplemod:item_b"
    ],

    // (Optional) Which enchantments are incompatible with this one.
    // Can be an enchantment id, such as "minecraft:sharpness",
    // or a list of enchantment ids, such as ["minecraft:sharpness", "minecraft:fire_aspect"],
    // or enchantment tag, such as "#examplemod:exclusive_to_enchant_name".
    // Incompatible enchantments will not be added to the same item by vanilla mechanics.
    "exclusive_set": "#examplemod:exclusive_to_enchant_name",
    
    // The likelihood that this enchantment will appear in the Enchanting Table. 
    // Bounded by [1, 1024].
    "weight": 6,
    
    // The maximum level this enchantment is allowed to reach.
    // Bounded by [1, 255].
    "max_level": 3,
    
    // The maximum cost of this enchantment, measured in "enchanting power". 
    // This corresponds to, but is not equivalent to, the threshold in levels the player needs to meet to bestow this enchantment.
    // See below for details.
    // The actual cost will be between this and the min_cost.
    "max_cost": {
        "base": 45,
        "per_level_above_first": 9
    },
    
    // Specifies the minimum cost of this enchantment; otherwise as above.
    "min_cost": {
        "base": 2,
        "per_level_above_first": 8
    },

    // The cost that this enchantment adds to repairing an item in an anvil in levels. The cost is multiplied by enchantment level.
    // If an item has a DataComponentTypes.STORED_ENCHANTMENTS component, the cost is halved. In vanilla, this only applies to enchanted books.
    // Bounded by [1, inf).
    "anvil_cost": 2,
    
    // (Optional) A list of slot groups this enchantment provides effects in. 
    // A slot group is defined as one of the possible values of the EquipmentSlotGroup enum.
    // In vanilla, these are: `any`, `hand`, `mainhand`, `offhand`, `armor`, `feet`, `legs`, `chest`, `head`, and  `body`.
    "slots": [
        "mainhand"
    ],

    // The effects that this enchantment provides as a map of enchantment effect components (read on).
    "effects": {
        "examplemod:custom_effect": [
            {
                "effect": {
                    "type": "minecraft:add",
                    "value": {
                        "type": "minecraft:linear",
                        "base": 1,
                        "per_level_above_first": 1
                    }
                }
            }
        ]
    }
}

附魔成本和等级

max_cost 与 min_cost 规定生成附魔所需“附魔能力”的范围,实际使用过程较复杂。

附魔台先读取周围方块的 IBlockExtension#getEnchantPowerBonus(),再调用 EnchantmentHelper#getEnchantmentCost 得到每个槽位的基础等级,即界面中附魔旁的绿色数字。随后依据物品附魔能力(IItemExtension#getEnchantmentValue() 的返回值)产生两次随机修正:

修正等级 = 基础等级 + random.nextInt(e / 4 + 1) + random.nextInt(e / 4 + 1),其中 e 是物品附魔能力。

该结果再随机上下浮动 15%,最终用于选择附魔;只有落在附魔成本范围内的附魔才会被选中。

因此,附魔定义的成本可能高于 30,甚至高出很多。例如,附魔能力为 10 的物品可达到 1.15 * (30 + 2 * (10 / 4) + 1) = 40 的成本。

附魔效果组件

附魔效果组件是专门注册的数据组件,决定附魔行为。组件类型定义效果,所含数据提供或调整效果信息。例如,minecraft:damage 按照数据指定的数值修改武器伤害。

原版定义了多种内置效果组件,用来实现全部原版附魔。

自定义效果组件

自定义组件的应用逻辑必须由作者完整实现。先定义类或 record 保存效果所需信息,例如 Increment:

// Define an example data-bearing record.
public record Increment(int value) {
    public static final Codec<Increment> CODEC = RecordCodecBuilder.create(instance ->
            instance.group(
                    Codec.INT.fieldOf("value").forGetter(Increment::value)
            ).apply(instance, Increment::new)
    );

    public int add(int x) {
        return value() + x;
    }
}

效果组件类型必须注册到 BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE,该注册表接收 DataComponentType<?>。例如,注册保存 Increment 对象的类型:

// In some registration class
public static final DeferredRegister<DataComponentType<?>> ENCHANTMENT_COMPONENT_TYPES = DeferredRegister.create(BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, "examplemod");

public static final DeferredHolder<DataComponentType<?>, DataComponentType<Increment>>> INCREMENT =
    ENCHANTMENT_COMPONENT_TYPES.register("increment",
        () -> DataComponentType.<Increment>builder()
            .persistent(Increment.CODEC)
            .build());

接着实现使用组件修改整数的游戏逻辑:

// Somewhere in game logic where an `itemStack` is available.
// `INCREMENT` is the enchantment component type holder defined above.
// `value` is an integer.
AtomicInteger atomicValue = new AtomicInteger(value);

EnchantmentHelper.runIterationOnItem(stack, (enchantmentHolder, enchantLevel) -> {
    // Acquire the Increment instance from the enchantment holder (or null if this is a different enchantment)
    Increment increment = enchantmentHolder.value().effects().get(INCREMENT.get());

    // If this enchant has an Increment component, use it.
    if(increment != null){
        atomicValue.set(increment.add(atomicValue.get()));
    }
});

int modifiedValue = atomicValue.get();
// Use the now-modified value elsewhere in your game logic.

首先调用 EnchantmentHelper#runIterationOnItem 的某个重载。它接受 EnchantmentHelper.EnchantmentVisitor 函数接口,并以附魔及等级为参数,对物品堆的全部附魔执行操作(本质上类似 BiConsumer<Enchantment, Integer>)。

使用 Increment#add 执行修改。因为位于 lambda 内,需要 AtomicInteger 等可原子更新的类型。这样同一物品上的多个 INCREMENT 组件也能依次叠加效果,与原版行为类似。

ConditionalEffect

用 ConditionalEffect<?> 包装类型,可根据给定 LootContext 决定是否生效。

ConditionalEffect#matches(LootContext context) 依据内部 Optional<LootItemConditon> 判断能否运行,并处理 LootItemCondition 的序列化与反序列化。

原版的 Enchantment#applyEffects() 进一步简化了条件检查:它接受 List<ConditionalEffect<T>>,检查每个条件,再对通过条件的 T 执行 Consumer<T>。许多原版组件本身就是这种列表,可直接使用:

// `enchant` is an Enchantment instance.
// `lootContext` is a LootContext instance.
Enchantment.applyEffects(
    enchant.getEffects(EnchantmentEffectComponents.KNOCKBACK), // Or whichever other List<ConditionalEffect<T>> you want
    lootContext,
    (effectData) -> // Use the effectData (in this example, an EnchantmentValueEffect) however you want.
);

自定义的 ConditionalEffect 包装组件可这样注册:

public static final DeferredHolder<DataComponentType<?>, DataComponentType<ConditionalEffect<Increment>>> CONDITIONAL_INCREMENT =
    ENCHANTMENT_COMPONENT_TYPES.register("conditional_increment",
        () -> DataComponentType.ConditionalEffect<Increment>builder()
            // The LootContextParamSet needed depends on what the enchantment is supposed to do.
            // This might be one of ENCHANTED_DAMAGE, ENCHANTED_ITEM, ENCHANTED_LOCATION, ENCHANTED_ENTITY, or HIT_BLOCK
            // since all of these bring the enchantment level into context (along with whatever other information is indicated).
            .persistent(ConditionalEffect.codec(Increment.CODEC, LootContextParamSets.ENCHANTED_DAMAGE))
            .build());

ConditionalEffect.codec 的参数依次是泛型 ConditionalEffect<T> 对应的 codec,以及 LootContextParamSets 中的一项。

附魔数据生成

把 RegistrySetBuilder 传给 DatapackBuiltInEntriesProvider,即可通过数据生成创建附魔 JSON,输出到 <project root>/src/generated/data/<modid>/enchantment/<path>.json。

关于 RegistrySetBuilder 和 DatapackBuiltinEntriesProvider,见数据包注册表的数据生成。

数据生成


// This RegistrySetBuilder should be passed into a DatapackBuiltinEntriesProvider in your GatherDataEvent handler.
RegistrySetBuilder BUILDER = new RegistrySetBuilder();
BUILDER.add(
    Registries.ENCHANTMENT,
    bootstrap -> bootstrap.register(
        // Define the ResourceKey for our enchantment.
        ResourceKey.create(
            Registries.ENCHANTMENT,
            ResourceLocation.fromNamespaceAndPath("examplemod", "example_enchantment")
        ),
        new Enchantment(
            // The text Component that specifies the enchantment's name.
            Component.literal("Example Enchantment"),  
            
            // Specify the enchantment definition of for our enchantment.
            new Enchantment.EnchantmentDefinition(
                // A HolderSet of Items that the enchantment will be compatible with.
                HolderSet.direct(...), 

                // An Optional<HolderSet> of items that the enchantment considers "primary".
                Optional.empty(), 

                // The weight of the enchantment.
                30, 

                // The maximum level this enchantment can be.
                3, 

                // The minimum cost of the enchantment. The first parameter is base cost, the second is cost per level.
                new Enchantment.Cost(3, 1), 

                // The maximum cost of the enchantment. As above.
                new Enchantment.Cost(4, 2), 

                // The anvil cost of the enchantment.
                2, 

                // A list of EquipmentSlotGroups that this enchantment has effects in.
                List.of(EquipmentSlotGroup.ANY) 
            ),
            // A HolderSet of incompatible other enchantments.
            HolderSet.empty(), 

            // A DataComponentMap of the enchantment effect components associated with this enchantment and their values.
            DataComponentMap.builder() 
                .set(MY_ENCHANTMENT_EFFECT_COMPONENT_TYPE, new ExampleData())
                .build()
        )
    )
);

JSON

// For more detail on each entry, please check the section above on the enchantment JSON format.
{
    // The anvil cost of the enchantment.
    "anvil_cost": 2,

    // The text Component that specifies the enchantment's name.
    "description": "Example Enchantment",

    // A map of the effect components associated with this enchantment and their values.
    "effects": {
        // <effect components>
    },

    // The maximum cost of the enchantment.
    "max_cost": {
        "base": 4,
        "per_level_above_first": 2
    },

    // The maximum level this enchantment can be.
    "max_level": 3,

    // The minimum cost of the enchantment.
    "min_cost": {
        "base": 3,
        "per_level_above_first": 1
    },

    // A list of EquipmentSlotGroup aliases that this enchantment has effects in.
    "slots": [
        "any"
    ],

    // The set of items that this enchantment can be applied to using an anvil.
    "supported_items": /* <supported item list> */,

    // The weight of this enchantment.
    "weight": 30
}

来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交 4a680cdd4d15。