Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a guide for Custom Enchantments #134

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export default [
text: "develop.items.custom-item-interactions",
link: "/develop/items/custom-item-interactions"
},
{
text: "develop.items.custom-enchantment-effects",
link: "/develop/items/custom-enchantment-effects"
},
{
text: "develop.items.custom-data-components",
link: "/develop/items/custom-data-components"
Expand Down
62 changes: 62 additions & 0 deletions develop/items/custom-enchantment-effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Custom Enchantment Effects
description: Learn how to create your enchantment effects.
authors:
- krizh-p
---

# Custom Enchantments {#custom-enchantments}

Starting from version 1.21, custom enchantments in Minecraft use a "data-driven" approach. This makes it easier to add simple enchantments, like increasing attack damage, but more challenging to create complex ones. The process involves breaking down enchantments into _effect components_.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting from version 1.21

Does this have to be mentioned here? The plugin already highlights it.

in Minecraft

Again, the entire Fabric project revolves around Minecraft, no need to specify it in every page IMO.


An effect component contains the code that defines the special effects of an enchantment. Minecraft supports various default effects, such as item damage, knockback, and experience.

Check failure on line 12 in develop/items/custom-enchantment-effects.md

View workflow job for this annotation

GitHub Actions / markdownlint

Trailing spaces

develop/items/custom-enchantment-effects.md:12:182 MD009/no-trailing-spaces Trailing spaces [Expected: 0; Actual: 1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An effect component contains the code that defines the special effects of an enchantment. Minecraft supports various default effects, such as item damage, knockback, and experience.
An effect component contains the code that defines the special effects of an enchantment. Minecraft supports various default effects, such as item damage, knockback, and experience.


However, this guide focuses on creating custom enchantment effects that aren't supported by default.

**Before continuing, please check if the default Minecraft effects can work for your needs by visiting [the Minecraft Wiki's Enchantment Effect Components page](https://minecraft.wiki/w/Enchantment_definition#Effect_components)**. The rest of the guide assumes you understand how to configure "simple" data-driven enchantments.
Comment on lines +12 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An effect component contains the code that defines the special effects of an enchantment. Minecraft supports various default effects, such as item damage, knockback, and experience.
However, this guide focuses on creating custom enchantment effects that aren't supported by default.
**Before continuing, please check if the default Minecraft effects can work for your needs by visiting [the Minecraft Wiki's Enchantment Effect Components page](https://minecraft.wiki/w/Enchantment_definition#Effect_components)**. The rest of the guide assumes you understand how to configure "simple" data-driven enchantments.
An effect component contains the code that defines the special effects of an enchantment. Minecraft supports various default effects, such as item damage, knockback, and experience.
::: tip
Be sure to check if the default Minecraft effects satisfy your needs by visiting [the Minecraft Wiki's Enchantment Effect Components page](https://minecraft.wiki/w/Enchantment_definition#Effect_components). This guide assumes you understand how to configure "simple" data-driven enchantments and focuses on creating custom enchantment effects that aren't supported by default.
:::


## Custom Enchantment Effects {#custom-enchantment-effects}

Start by creating an `enchantment` folder, and within it create a folder `effect`. Within the `effect` folder, we'll create the `LightningEnchantmentEffect` record.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Start by creating an `enchantment` folder, and within it create a folder `effect`. Within the `effect` folder, we'll create the `LightningEnchantmentEffect` record.
Start by creating an `enchantment` folder, and within it, create an `effect` folder. Within that, we'll create the `LightningEnchantmentEffect` record.


Next, we can create a constructor and override the `EnchantmentEntityEffect` interface methods. We'll also create `CODEC` variable to encode and decode our effect; you can read more about [Codecs here](../codecs).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Next, we can create a constructor and override the `EnchantmentEntityEffect` interface methods. We'll also create `CODEC` variable to encode and decode our effect; you can read more about [Codecs here](../codecs).
Next, we can create a constructor and override the `EnchantmentEntityEffect` interface methods. We'll also create a `CODEC` variable to encode and decode our effect; you can read more about [Codecs here](../codecs).


The bulk of our code will go into the `apply()` event, which is called when the criteria for your enchantment to work is met. We'll later configure this `Effect` to be called when an entity is hit, but for now let's write simple code to strike the target with lightning.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The bulk of our code will go into the `apply()` event, which is called when the criteria for your enchantment to work is met. We'll later configure this `Effect` to be called when an entity is hit, but for now let's write simple code to strike the target with lightning.
The bulk of our code will go into the `apply()` event, which is called when the criteria for your enchantment to work is met. We'll later configure this `Effect` to be called when an entity is hit, but for now, let's write simple code to strike the target with lightning.


@[code transcludeWith=#entrypoint](@/reference/latest/src/main/java/com/example/docs/enchantment/effect/LightningEnchantmentEffect.java)

Here the `amount` variable indicates a value scaled to the level of the enchantment. We can use this to modify how effective the enchantment is based on level. In the code above, we are using the level of the enchantment to determine how many lightning strikes are spawned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Here the `amount` variable indicates a value scaled to the level of the enchantment. We can use this to modify how effective the enchantment is based on level. In the code above, we are using the level of the enchantment to determine how many lightning strikes are spawned.
Here, the `amount` variable indicates a value scaled to the level of the enchantment. We can use this to modify how effective the enchantment is based on level. In the code above, we are using the level of the enchantment to determine how many lightning strikes are spawned.


## Registering the Enchantment Effect {#registering-the-enchantment-effect}

Like every other component of your mod, we'll have to add this `EnchantmentEffect` to Minecraft's registry. To do so, add a class `ModEnchantmentEffects` (or whatever you want to name it) and a helper method to register the enchantment. Be sure to call the `registerModEnchantmentEffects()` in your main class which contains the `onInitialize()` method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Like every other component of your mod, we'll have to add this `EnchantmentEffect` to Minecraft's registry. To do so, add a class `ModEnchantmentEffects` (or whatever you want to name it) and a helper method to register the enchantment. Be sure to call the `registerModEnchantmentEffects()` in your main class which contains the `onInitialize()` method.
Like every other component of your mod, we'll have to add this `EnchantmentEffect` to Minecraft's registry. To do so, add a class `ModEnchantmentEffects` (or whatever you want to name it) and a helper method to register the enchantment. Be sure to call the `registerModEnchantmentEffects()` in your main class, which contains the `onInitialize()` method.


@[code transcludeWith=#entrypoint](@/reference/latest/src/main/java/com/example/docs/enchantment/ModEnchantmentEffects.java)
its-miroma marked this conversation as resolved.
Show resolved Hide resolved

## Creating the Enchantment {#creating-the-enchantment}

Now we have an enchantment effect! The final step is to create an enchantment that applies our custom effect. While this can be done by creating a JSON file similar to those in datapacks, this guide will show you how to generate the JSON dynamically using Fabric's data generation tools. To begin, create an `EnchantmentGenerator` class.

Within this class, we'll first register a new enchantment, and then use the `configure()` method to create our JSON programmatically.

@[code transcludeWith=#entrypoint](@/reference/latest/src/main/java/com/example/docs/data/EnchantmentGenerator.java)

Before proceeding, you should ensure your project is configured for data generation; if you are unsure, [view the respective wiki page](https://fabricmc.net/wiki/tutorial:datagen_setup).

Lastly, we must tell our mod to add our `EnchantmentGenerator` to the list of data generation tasks. To do so, simply add the `EnchantmentGenerator` to this inside of the `onInitializeDataGenerator` class.

@[code transcludeWith=#initdatagen](@/reference/latest/src/main/java/com/example/docs/FabricDocsReferenceDataGenerator.java)

Now, when you run your mod's data generation task, enchantment JSONs will be generated inside the `generated` folder. An example can be seen below:

@[code](@/reference/latest/src/main/generated/data/fabric-docs-reference/enchantment/thundering.json)

You should also add translations to your `en_us.json` file to give your enchantment a readable name:

```json
"enchantment.FabricDocsReference.thundering": "Thundering",
```

You should now have a working custom enchantment effect! You can test it by enchanting a weapon with the enchantment and hitting a mob, an example is given in the following video.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You should now have a working custom enchantment effect! You can test it by enchanting a weapon with the enchantment and hitting a mob, an example is given in the following video.
You should now have a working custom enchantment effect! Test it by enchanting a weapon with the enchantment and hitting a mob. An example is given in the following video:


<VideoPlayer src="/assets/develop/enchantment-effects/thunder.webm" title="Using the Lightning Effect" />
krizh-p marked this conversation as resolved.
Show resolved Hide resolved
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// 1.21 2024-07-09T13:17:21.830983 Fabric docs reference/null
b0b8d77fe2f55934f146827951450686de077dac data\fabric-docs-reference\enchantment\thundering.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"anvil_cost": 5,
"description": {
"translate": "enchantment.fabric-docs-reference.thundering"
},
"effects": {
"minecraft:post_attack": [
{
"affected": "victim",
"effect": {
"type": "fabric-docs-reference:lightning_effect",
"amount": {
"type": "minecraft:linear",
"base": 0.4,
"per_level_above_first": 0.2
}
},
"enchanted": "attacker"
}
]
},
"max_cost": {
"base": 1,
"per_level_above_first": 15
},
"max_level": 3,
"min_cost": {
"base": 1,
"per_level_above_first": 10
},
"slots": [
"hand"
],
"supported_items": "#minecraft:enchantable/weapon",
"weight": 10
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"replace": false,
"values": [
"fabric-docs-reference:tater"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;

import com.example.docs.data.EnchantmentGenerator;

public class FabricDocsReferenceDataGenerator implements DataGeneratorEntrypoint {
//#initdatagen
@Override
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();
pack.addProvider(EnchantmentGenerator::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;

import com.example.docs.data.EnchantmentGenerator;

public class FabricDocsReferenceDamageTypesDataGenerator implements DataGeneratorEntrypoint {
public static final DamageType TATER_DAMAGE_TYPE = new DamageType("tater", DamageScaling.WHEN_CAUSED_BY_LIVING_NON_PLAYER, 0.1f);

Expand All @@ -36,6 +38,7 @@ public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {

pack.addProvider(TaterDamageTypesGenerator::new);
pack.addProvider(TaterDamageTypeTagGenerator::new);
pack.addProvider(EnchantmentGenerator::new);
}

private static class TaterDamageTypeTagGenerator extends FabricTagProvider<DamageType> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.example.docs.data;

import java.util.concurrent.CompletableFuture;

import net.minecraft.component.EnchantmentEffectComponentTypes;
import net.minecraft.component.type.AttributeModifierSlot;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentLevelBasedValue;
import net.minecraft.enchantment.effect.EnchantmentEffectTarget;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.tag.ItemTags;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition;

import com.example.docs.FabricDocsReference;
import com.example.docs.enchantment.effect.LightningEnchantmentEffect;

//#entrypoint
public class EnchantmentGenerator extends FabricDynamicRegistryProvider {
public static final RegistryKey<Enchantment> THUNDERING = EnchantmentGenerator.of("thundering");

public EnchantmentGenerator(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registriesFuture);
System.out.println("REGISTERING ENCHANTS");
}

@Override
protected void configure(RegistryWrapper.WrapperLookup registries, Entries entries) {
// Our new enchantment, "Thundering."
register(entries, THUNDERING, Enchantment.builder(
Enchantment.definition(
registries.getWrapperOrThrow(RegistryKeys.ITEM).getOrThrow(ItemTags.WEAPON_ENCHANTABLE),
// this is the "weight" or probability of our enchantment showing up in the table
10,
// the maximum level of the enchantment
3,
// base cost for level 1 of the enchantment, and min levels required for something higher
Enchantment.leveledCost(1, 10),
// same fields as above but for max cost
Enchantment.leveledCost(1, 15),
// anvil cost
5,
// valid slots
AttributeModifierSlot.HAND
)
)
.addEffect(
krizh-p marked this conversation as resolved.
Show resolved Hide resolved
// enchantment occurs POST_ATTACK
EnchantmentEffectComponentTypes.POST_ATTACK,
EnchantmentEffectTarget.ATTACKER,
EnchantmentEffectTarget.VICTIM,
new LightningEnchantmentEffect(EnchantmentLevelBasedValue.linear(0.4f, 0.2f)) // scale the enchantment linearly.
)
);
}

private void register(Entries entries, RegistryKey<Enchantment> key, Enchantment.Builder builder, ResourceCondition... resourceConditions) {
entries.add(key, builder.build(key.getValue()), resourceConditions);
}

private static RegistryKey<Enchantment> of(String path) {
Identifier id = Identifier.of(FabricDocsReference.MOD_ID, path);
return RegistryKey.of(RegistryKeys.ENCHANTMENT, id);
}

@Override
public String getName() {
return "ReferenceDocEnchantmentGenerator";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.docs.enchantment;

import net.fabricmc.api.ModInitializer;

public class FabricDocsReferenceEnchantments implements ModInitializer {
@Override
public void onInitialize() {
ModEnchantmentEffects.registerModEnchantmentEffects();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.docs.enchantment;

import com.mojang.serialization.MapCodec;

import net.minecraft.enchantment.effect.EnchantmentEntityEffect;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;

import com.example.docs.FabricDocsReference;
import com.example.docs.enchantment.effect.LightningEnchantmentEffect;

//#entrypoint
public class ModEnchantmentEffects {
public static MapCodec<LightningEnchantmentEffect> LIGHTNING_EFFECT = register("lightning_effect", LightningEnchantmentEffect.CODEC);

private static <T extends EnchantmentEntityEffect> MapCodec<T> register(String id, MapCodec<T> codec) {
return Registry.register(Registries.ENCHANTMENT_ENTITY_EFFECT_TYPE, Identifier.of(FabricDocsReference.MOD_ID, id), codec);
}

public static void registerModEnchantmentEffects() {
FabricDocsReference.LOGGER.info("Registering EnchantmentEffects for" + FabricDocsReference.MOD_ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.docs.enchantment.effect;

import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;

import net.minecraft.enchantment.EnchantmentEffectContext;
import net.minecraft.enchantment.EnchantmentLevelBasedValue;
import net.minecraft.enchantment.effect.EnchantmentEntityEffect;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.SpawnReason;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;

//#entrypoint
public record LightningEnchantmentEffect(EnchantmentLevelBasedValue amount) implements EnchantmentEntityEffect {
public static final MapCodec<LightningEnchantmentEffect> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
EnchantmentLevelBasedValue.CODEC.fieldOf("amount").forGetter(LightningEnchantmentEffect::amount)
).apply(instance, LightningEnchantmentEffect::new)
);

@Override
public void apply(ServerWorld world, int level, EnchantmentEffectContext context, Entity target, Vec3d pos) {
krizh-p marked this conversation as resolved.
Show resolved Hide resolved
if (target instanceof LivingEntity victim) {
if (context.owner() != null && context.owner() instanceof PlayerEntity player) {
float numStrikes = this.amount.getValue(level);

for (float i = 0; i < numStrikes; i++) {
BlockPos position = victim.getBlockPos();
EntityType.LIGHTNING_BOLT.spawn(world, position, SpawnReason.TRIGGERED);
}
}
}
}

@Override
public MapCodec<? extends EnchantmentEntityEffect> getCodec() {
return CODEC;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"itemGroup.fabric_docs_reference": "Fabric Docs Reference",
"block.fabric-docs-reference.condensed_dirt": "Condensed Dirt",
"block.fabric-docs-reference.condensed_oak_log": "Condensed Oak Log",
"enchantment.fabric-docs-reference.thundering": "Thundering",
"block.fabric-docs-reference.prismarine_lamp": "Prismarine Lamp"
}
1 change: 1 addition & 0 deletions reference/latest/src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"com.example.docs.damage.FabricDocsReferenceDamageTypes",
"com.example.docs.item.FabricDocsReferenceItems",
"com.example.docs.block.FabricDocsReferenceBlocks",
"com.example.docs.enchantment.FabricDocsReferenceEnchantments",
"com.example.docs.component.FabricDocsReferenceComponents"
],
"client": [
Expand Down
1 change: 1 addition & 0 deletions sidebar_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"develop.items.custom-tools": "Custom Tools",
"develop.items.custom-item-groups": "Custom Item Groups",
"develop.items.custom-item-interactions": "Custom Item Interactions",
"develop.items.custom-enchantment-effects": "Custom Enchantment Effects",
"develop.items.potions": "Potions",
"develop.items.custom-data-components": "Custom Data Components",
"develop.blocks": "Blocks",
Expand Down
Loading