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 14 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.potions",
link: "/develop/items/potions",
Expand Down
60 changes: 60 additions & 0 deletions develop/items/custom-enchantment-effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Custom Enchantment Effects
description: Learn how to create your enchantment effects.
authors:
- krizh-p
---

# Custom Enchantments {#custom-enchantments}

Since 1.21, custom enchantments have taken a "data-driven" approach; while this makes adding simple enchantments (like increasing attack damage) easier and more straightforward, it also complicates creating complex enchantments. This was done by breaking down what enchantments do into _effect components_.

An effect component contains code for what special things an enchantment should do. By default, Minecraft supports various effects such as item damage, knockback, experience, and more--however, this guide will focus on creating custom enchantment effects that are not supported by default.

**[You are heavily suggested to first determine if the default Minecraft effects will work for your use case before continuing](https://minecraft.wiki/w/Enchantment_definition#Effect_components)**. The rest of the guide will assume you understand how "simple" data-driven enchantments are configured.

## 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; for more information on Codecs, [see their respective wiki page](https://docs.fabricmc.net/develop/codecs).
IMB11 marked this conversation as resolved.
Show resolved Hide resolved

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.

@[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! Lastly, we'll create an enchantment to apply our custom effect to. This can be done by creating a JSON file like in datapacks, however, in this guide we'll generate the JSON dynamically using Fabric's data generation tools. To start, 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)
krizh-p marked this conversation as resolved.
Show resolved Hide resolved
IMB11 marked this conversation as resolved.
Show resolved Hide resolved

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)
IMB11 marked this conversation as resolved.
Show resolved Hide resolved

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)
IMB11 marked this conversation as resolved.
Show resolved Hide resolved

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 be able to see our enchantment by running the Client task and opening up Minecraft.

<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 @@ -11,6 +11,8 @@
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes;

import com.example.docs.enchantment.ModEnchantmentEffects;

//#entrypoint
public class FabricDocsReference implements ModInitializer {
// This logger is used to write text to the console and the log file.
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() {
System.out.println("Registering EnchantmentEffects for" + FabricDocsReference.MOD_ID);
krizh-p marked this conversation as resolved.
Show resolved Hide resolved
}
}
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 @@ -14,5 +14,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"
}
3 changes: 2 additions & 1 deletion reference/latest/src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"com.example.docs.sound.FabricDocsReferenceSounds",
"com.example.docs.damage.FabricDocsReferenceDamageTypes",
"com.example.docs.item.FabricDocsReferenceItems",
"com.example.docs.block.FabricDocsReferenceBlocks"
"com.example.docs.block.FabricDocsReferenceBlocks",
"com.example.docs.enchantment.FabricDocsReferenceEnchantments"
],
"client": [
"com.example.docs.FabricDocsReferenceClient",
Expand Down
1 change: 1 addition & 0 deletions sidebar_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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.blocks": "Blocks",
"develop.blocks.first-block": "Creating Your First Block",
Expand Down