Skip to content

These docs were made completely by AI, so they might be right, or wrong, you'll need to test them yourself. This was made for a easier understanding of everything. So use at your own risk. If anything is wrong, please don't hurt to make a PR on the page you have a problem with. ON GITHUB

Custom Enchantments

import { Aside } from ‘@astrojs/starlight/components’;

This guide goes beyond the basics covered in Adding Enchantments. Here you’ll learn how the enchantment system actually works under the hood, how to bend the conflict rules, push enchantment levels past their normal limits, and build enchantments that do things the vanilla system never did.

If you haven’t read the adding enchantments guide yet, start there. This page assumes you already know how to create a basic Enchantment subclass and register it.

Every enchantment in the game inherits from the Enchantment base class in Minecraft.World/Enchantment.h. The class manages a global registry, frequency-based weighting for the enchanting table, and virtual methods that subclasses override to define behavior.

Enchantments live in a static array of 256 slots:

Enchantment.h
static EnchantmentArray enchantments; // 256 entries
static vector<Enchantment *> validEnchantments;

When you construct an Enchantment, the _init() method slots it into the array by ID:

void Enchantment::_init(int id)
{
if (enchantments[id] != NULL)
{
app.DebugPrintf("Duplicate enchantment id!");
__debugbreak();
}
enchantments[id] = this;
}

After all enchantments are created in staticCtor(), a loop at the end collects every non-null entry into validEnchantments. This second list is what the enchanted book loot system uses to pick random enchantments.

The frequency field controls how likely an enchantment is to be selected when the enchanting table is picking candidates. It’s the weight used in WeighedRandom selection. Four constants are defined:

ConstantValueWhat it means
FREQ_COMMON10Shows up a lot. Protection, Sharpness, Efficiency, Power.
FREQ_UNCOMMON5Less common. Fire Protection, Smite, Knockback, Unbreaking.
FREQ_RARE2Hard to get. Blast Protection, Fire Aspect, Looting, Fortune, Flame, Punch.
FREQ_VERY_RARE1Almost never shows up. Silk Touch, Thorns, Infinity.

Higher frequency = more likely to be picked. A FREQ_COMMON enchantment is 10x more likely to be chosen than a FREQ_VERY_RARE one when both are in the candidate pool.

EnchantmentCategory determines which items an enchantment can go on. The check happens through canEnchant(Item *item) which uses dynamic_cast to figure out the item type:

bool EnchantmentCategory::canEnchant(Item *item) const
{
if (this == all) return true;
if (dynamic_cast<ArmorItem *>(item) != NULL)
{
if (this == armor) return true;
ArmorItem *ai = (ArmorItem *) item;
if (ai->slot == ArmorItem::SLOT_HEAD) return this == armor_head;
if (ai->slot == ArmorItem::SLOT_LEGS) return this == armor_legs;
if (ai->slot == ArmorItem::SLOT_TORSO) return this == armor_torso;
if (ai->slot == ArmorItem::SLOT_FEET) return this == armor_feet;
return false;
}
else if (dynamic_cast<WeaponItem *>(item) != NULL)
return this == weapon;
else if (dynamic_cast<DiggerItem *>(item) != NULL)
return this == digger;
else if (dynamic_cast<BowItem *>(item) != NULL)
return this == bow;
return false;
}

This is why enchantments like Sharpness don’t normally show up on pickaxes. The category check gates what’s even considered as a candidate.

Here’s every enchantment in the game with its internal details:

IDNameClassTypeCategoryFreqMax Lvl
0ProtectionProtectionEnchantmentALLarmor104
1Fire ProtectionProtectionEnchantmentFIREarmor54
2Feather FallingProtectionEnchantmentFALLarmor_feet54
3Blast ProtectionProtectionEnchantmentEXPLOSIONarmor24
4Projectile ProtectionProtectionEnchantmentPROJECTILEarmor54
5RespirationOxygenEnchantment-armor_head23
6Aqua AffinityWaterWorkerEnchantment-armor_head21
7ThornsThornsEnchantment-armor_torso13
IDNameClassTypeCategoryFreqMax Lvl
16SharpnessDamageEnchantmentALLweapon105
17SmiteDamageEnchantmentUNDEADweapon55
18Bane of ArthropodsDamageEnchantmentARTHROPODSweapon55
19KnockbackKnockbackEnchantment-weapon52
20Fire AspectFireAspectEnchantment-weapon22
21LootingLootBonusEnchantment-weapon23
IDNameClassTypeCategoryFreqMax Lvl
32EfficiencyDiggingEnchantment-digger105
33Silk TouchUntouchingEnchantment-digger11
34UnbreakingDigDurabilityEnchantment-digger53
35FortuneLootBonusEnchantment-digger23
IDNameClassTypeCategoryFreqMax Lvl
48PowerArrowDamageEnchantment-bow105
49PunchArrowKnockbackEnchantment-bow22
50FlameArrowFireEnchantment-bow21
51InfinityArrowInfiniteEnchantment-bow11

IDs 8-15, 22-31, 36-47, and 52-255 are all free to use. Pick something that makes sense. If you’re adding more armor enchantments, use 8+. More weapon stuff, start at 22. You get the idea.

The base isCompatibleWith() is simple. An enchantment is only incompatible with itself:

bool Enchantment::isCompatibleWith(Enchantment *other) const
{
return this != other;
}

So by default, any two different enchantments can coexist on the same item.

The protection enchantments have the most interesting conflict logic. Protection types (Protection, Fire Protection, Blast Protection, Projectile Protection) are all mutually exclusive with each other. But Feather Falling is the exception. It plays nice with all of them:

bool ProtectionEnchantment::isCompatibleWith(Enchantment *other) const
{
ProtectionEnchantment *pe =
dynamic_cast<ProtectionEnchantment *>(other);
if (pe != NULL)
{
// Same type? Always incompatible.
if (pe->type == this->type) return false;
// Feather Falling gets a pass.
if (this->type == FALL || pe->type == FALL) return true;
// Everything else conflicts.
return false;
}
return Enchantment::isCompatibleWith(other);
}

This means you can have Feather Falling + Protection on boots, but you can’t have Protection + Fire Protection on the same chestplate.

All three damage enchantments (Sharpness, Smite, Bane of Arthropods) are mutually exclusive. The check is a one-liner:

bool DamageEnchantment::isCompatibleWith(Enchantment *other) const
{
return dynamic_cast<DamageEnchantment *>(other) == NULL;
}

If the other enchantment is any kind of DamageEnchantment, they conflict.

Silk Touch and Fortune conflict with each other through both sides. UntouchingEnchantment checks:

bool UntouchingEnchantment::isCompatibleWith(Enchantment *other) const
{
return Enchantment::isCompatibleWith(other)
&& other->id != resourceBonus->id;
}

And LootBonusEnchantment checks the reverse:

bool LootBonusEnchantment::isCompatibleWith(Enchantment *other) const
{
return Enchantment::isCompatibleWith(other)
&& other->id != untouching->id;
}

Conflicts are enforced in two places:

  1. Enchanting table: EnchantmentHelper::selectEnchantment() removes incompatible candidates each round when building multi-enchantment results.
  2. Anvil: RepairMenu::createResult() checks isCompatibleWith() against all existing enchantments on the input item. Incompatible enchantments get skipped (but still charge a cost as an “incompatibility fee”).
// From selectEnchantment - enchanting table conflict check:
for (auto it = availableEnchantments->begin();
it != availableEnchantments->end();)
{
int nextEnchantment = it->first;
bool valid = true;
for (auto resIt = results->begin();
resIt != results->end(); ++resIt)
{
EnchantmentInstance *current = *resIt;
if (!current->enchantment->isCompatibleWith(
Enchantment::enchantments[nextEnchantment]))
{
valid = false;
break;
}
}
if (!valid)
it = availableEnchantments->erase(it);
else
++it;
}

Making Conflicting Enchantments Work Together

Section titled “Making Conflicting Enchantments Work Together”

Want Protection + Fire Protection on the same piece of armor? Or Sharpness + Smite on the same sword? You just need to change the isCompatibleWith() method.

Example: Allow All Protection Types Together

Section titled “Example: Allow All Protection Types Together”

Override ProtectionEnchantment::isCompatibleWith() to always return true for other protection types:

bool ProtectionEnchantment::isCompatibleWith(Enchantment *other) const
{
// Allow all protection types to coexist
return Enchantment::isCompatibleWith(other);
}

That’s it. The base class check (this != other) already prevents duplicate enchantments. By removing the ProtectionEnchantment-specific logic, all protection types can stack on the same armor piece.

Example: Allow Sharpness + Smite + Bane Together

Section titled “Example: Allow Sharpness + Smite + Bane Together”

Same idea for damage enchantments:

bool DamageEnchantment::isCompatibleWith(Enchantment *other) const
{
// Allow all damage types to coexist
return Enchantment::isCompatibleWith(other);
}

Now a sword can have Sharpness V, Smite V, and Bane of Arthropods V all at once.

Remove the cross-check from both sides:

UntouchingEnchantment.cpp
bool UntouchingEnchantment::isCompatibleWith(Enchantment *other) const
{
return Enchantment::isCompatibleWith(other);
}
// LootBonusEnchantment.cpp
bool LootBonusEnchantment::isCompatibleWith(Enchantment *other) const
{
return Enchantment::isCompatibleWith(other);
}

Making a New Enchantment Conflict With Existing Ones

Section titled “Making a New Enchantment Conflict With Existing Ones”

If your custom enchantment should conflict with something specific, override isCompatibleWith() and check by ID or by type:

bool MyEnchantment::isCompatibleWith(Enchantment *other) const
{
// Incompatible with Knockback (ID 19)
if (other->id == Enchantment::knockback->id) return false;
// Incompatible with all DamageEnchantments
if (dynamic_cast<DamageEnchantment *>(other) != NULL) return false;
return Enchantment::isCompatibleWith(other);
}

The simplest change. Just return a higher number from getMaxLevel():

int MyEnchantment::getMaxLevel()
{
return 10; // Vanilla max is usually 5
}

But there’s a catch. The level display system in Enchantment::getLevelString() only has strings defined for levels 1 through 10:

wstring Enchantment::getLevelString(int level)
{
int stringId = IDS_ENCHANTMENT_LEVEL_1;
switch(level)
{
case 2: stringId = IDS_ENCHANTMENT_LEVEL_2; break;
case 3: stringId = IDS_ENCHANTMENT_LEVEL_3; break;
case 4: stringId = IDS_ENCHANTMENT_LEVEL_4; break;
case 5: stringId = IDS_ENCHANTMENT_LEVEL_5; break;
case 6: stringId = IDS_ENCHANTMENT_LEVEL_6; break;
case 7: stringId = IDS_ENCHANTMENT_LEVEL_7; break;
case 8: stringId = IDS_ENCHANTMENT_LEVEL_8; break;
case 9: stringId = IDS_ENCHANTMENT_LEVEL_9; break;
case 10: stringId = IDS_ENCHANTMENT_LEVEL_10; break;
};
return app.GetString(stringId);
}

If you go above level 10, the tooltip will just show the level 1 string (the default case). You’d need to either add more string IDs or override getFullname() to handle higher levels.

When you increase the max level, you also need to make sure your getMinCost() and getMaxCost() curves make sense. The enchanting table only picks a level if the calculated enchantment value falls within [getMinCost(level), getMaxCost(level)].

Here’s a practical example. Say you want Sharpness to go up to level 10. The vanilla DamageEnchantment cost curve is:

// Vanilla Sharpness (type ALL): minCost base = 1, levelCost = 11
int DamageEnchantment::getMinCost(int level)
{
return minCost[type] + (level - 1) * levelCost[type];
}
// Level 1: minCost = 1
// Level 5: minCost = 45
// Level 10: minCost = 100 <-- way beyond the table's max

The enchanting table’s max enchantment value tops out around 50 (with 15 bookshelves). So levels above 5 or 6 would never appear on the enchanting table. If you want higher levels to be obtainable through the table, compress the cost curve:

int MyDamageEnchantment::getMinCost(int level)
{
return 1 + (level - 1) * 5; // Tighter spacing
}
int MyDamageEnchantment::getMaxCost(int level)
{
return getMinCost(level) + 15; // Wider window
}

Or just accept that high levels are only available through commands and anvils.

Example: Raising All Vanilla Enchantment Levels

Section titled “Example: Raising All Vanilla Enchantment Levels”

If you want to blanket-raise the max level for all enchantments, you could modify the base class:

// In Enchantment.h, change the default:
virtual int getMaxLevel() { return 5; }

But that’s a blunt approach. Each subclass that already overrides getMaxLevel() won’t be affected. You’d need to change each subclass individually. A cleaner approach is to modify each one:

ProtectionEnchantment.cpp
int ProtectionEnchantment::getMaxLevel()
{
return 8; // Was 4
}
// DamageEnchantment.cpp
int DamageEnchantment::getMaxLevel()
{
return 10; // Was 5
}

Enchantments are stored as NBT data on the ItemInstance. The enchant() method on ItemInstance adds the enchantment ID and level to a tag list stored under the key "ench".

Each enchantment on an item is a CompoundTag with two short values:

  • TAG_ENCH_ID - the enchantment’s numeric ID
  • TAG_ENCH_LEVEL - the level

These tags are stored in a ListTag<CompoundTag> accessible via item->getEnchantmentTags().

EnchantmentHelper::getEnchantmentLevel() reads a specific enchantment’s level from an item:

int EnchantmentHelper::getEnchantmentLevel(
int enchantmentId, shared_ptr<ItemInstance> piece)
{
if (piece == NULL) return 0;
ListTag<CompoundTag> *enchantmentTags =
piece->getEnchantmentTags();
if (enchantmentTags == NULL) return 0;
for (int i = 0; i < enchantmentTags->size(); i++)
{
int type = enchantmentTags->get(i)->getShort(
(wchar_t *)ItemInstance::TAG_ENCH_ID);
int level = enchantmentTags->get(i)->getShort(
(wchar_t *)ItemInstance::TAG_ENCH_LEVEL);
if (type == enchantmentId) return level;
}
return 0;
}

There’s also an inventory-wide version that checks all items and returns the best (highest) level found.

EnchantmentHelper::setEnchantments() takes a map of {id: level} and writes them all to the item’s tag:

void EnchantmentHelper::setEnchantments(
unordered_map<int, int> *enchantments,
shared_ptr<ItemInstance> item)
{
ListTag<CompoundTag> *list = new ListTag<CompoundTag>();
for (auto it = enchantments->begin();
it != enchantments->end(); ++it)
{
CompoundTag *tag = new CompoundTag();
tag->putShort((wchar_t *)ItemInstance::TAG_ENCH_ID,
(short) it->first);
tag->putShort((wchar_t *)ItemInstance::TAG_ENCH_LEVEL,
(short) it->second);
list->add(tag);
}
if (list->size() > 0)
item->addTagElement(L"ench", list);
else if (item->hasTag())
item->getTag()->remove(L"ench");
}

Enchanted books store their enchantments under the "StoredEnchantments" tag instead of "ench". The EnchantedBookItem::addEnchantment() method handles this. It also has deduplication logic: if the book already has the enchantment at a lower level, it upgrades it instead of adding a duplicate.

When a player clicks a slot on the enchanting table, here’s what happens:

  1. EnchantmentMenu::clickMenuButton() gets called with the slot index (0-2)
  2. It checks costs[i] > 0, the item exists, and the player has enough levels (or is in creative)
  3. It calls EnchantmentHelper::selectEnchantment() to pick which enchantments and levels to apply
  4. For books: only one randomly chosen enchantment from the result is kept (randomIndex = random.nextInt(size))
  5. For regular items: all selected enchantments are applied via item->enchant()
  6. The player’s XP levels are withdrawn
  7. slotsChanged() is called to recalculate costs for any remaining operations

When a player attacks a mob, the game checks for damage enchantments through EnchantmentHelper::getDamageBonus(). Here’s the flow:

  1. The helper iterates over enchantments on the held item
  2. For each enchantment, it calls getDamageBonus(level, target)
  3. The results are summed up
  4. If the sum is greater than 0, the final bonus is 1 + random.nextInt(sum)

So the damage bonus is randomized. A Sharpness V sword has a max bonus of floor(5 * 2.75) = 13, but the actual bonus each hit will be somewhere between 1 and 13.

The DamageEnchantment class calculates per-level bonuses like this:

// Sharpness (ALL): 2.75 per level, works on everything
if (type == ALL)
return Mth::floor(level * 2.75f);
// Smite (UNDEAD): 4.5 per level, only vs undead
if (type == UNDEAD && target->getMobType() == UNDEAD)
return Mth::floor(level * 4.5f);
// Bane (ARTHROPODS): 4.5 per level, only vs arthropods
if (type == ARTHROPODS && target->getMobType() == ARTHROPOD)
return Mth::floor(level * 4.5f);

Protection enchantments feed into EnchantmentHelper::getDamageProtection():

  1. The helper iterates over all armor pieces
  2. For each enchantment on each piece, it calls getDamageProtection(level, source)
  3. Sum is capped at 25
  4. Final protection value is randomized: ((sum + 1) >> 1) + random.nextInt((sum >> 1) + 1)

The protection formula per enchantment type uses a base calculation of (6 + level * level) / 3.0f, then applies a multiplier based on the type:

TypeMultiplierWhen it applies
ALL (Protection)0.75xAll damage types
FIRE1.25xFire damage only
FALL2.5xFall damage only
EXPLOSION1.5xExplosion damage only
PROJECTILE1.5xProjectile damage only

Specialized protection is stronger against its specific damage type, but Protection (ALL) works against everything.

Fire Protection also reduces burn duration by 15% per level. Blast Protection reduces explosion knockback by 15% per level. These are handled separately in ProtectionEnchantment::getFireAfterDampener() and getExplosionKnockbackAfterDampener().

Thorns is a special case. It doesn’t use the standard getDamageProtection or getDamageBonus methods. Instead, ThornsEnchantment::doThornsAfterAttack() is called from the damage pipeline:

void ThornsEnchantment::doThornsAfterAttack(
shared_ptr<Entity> source, shared_ptr<Mob> target,
Random *random)
{
int level = EnchantmentHelper::getArmorThorns(target);
shared_ptr<ItemInstance> item =
EnchantmentHelper::getRandomItemWith(
Enchantment::thorns, target);
if (shouldHit(level, random))
{
source->hurt(DamageSource::thorns(target),
getDamage(level, random));
source->playSound(eSoundType_DAMAGE_THORNS,
.5f, 1.0f);
if (item != NULL)
item->hurt(3, target); // Extra durability cost
}
else
{
if (item != NULL)
item->hurt(1, target); // Still costs durability
}
}

Thorns always costs durability on the armor piece when the wearer is hit, even if it doesn’t trigger the reflect. When it does trigger, it costs 3 durability instead of 1.

Most other enchantments are checked by specific helper methods. Each one just calls getEnchantmentLevel() for the relevant enchantment ID:

Helper MethodEnchantmentChecked on
getKnockbackBonus()KnockbackHeld item
getFireAspect()Fire AspectCarried item
getOxygenBonus()RespirationArmor
getDiggingBonus()EfficiencyHeld item
getDigDurability()UnbreakingHeld item
hasSilkTouch()Silk TouchHeld item
getDiggingLootBonus()FortuneHeld item
getKillingLootBonus()LootingHeld item
hasWaterWorkerBonus()Aqua AffinityArmor

Unbreaking has a neat probability mechanic. Instead of reducing damage, it gives a random chance to skip durability loss entirely:

bool DigDurabilityEnchantment::shouldIgnoreDurabilityDrop(
shared_ptr<ItemInstance> item, int level, Random *random)
{
// Armor has a 40% chance to NOT benefit from Unbreaking
ArmorItem *armor = dynamic_cast<ArmorItem *>(item->getItem());
if (armor && random->nextFloat() < 0.6f) return false;
// Otherwise: (level)/(level+1) chance to ignore damage
return random->nextInt(level + 1) > 0;
}

So Unbreaking III on a non-armor item skips durability loss 75% of the time (3/4). On armor, it first has a 40% chance to not even check, making it less effective on armor pieces.

  1. Pick an unused ID (see the ID ranges table above)

  2. Add a static pointer in Enchantment.h:

static Enchantment *myEnchantment;
  1. Initialize it to NULL in Enchantment.cpp:
Enchantment *Enchantment::myEnchantment = NULL;
  1. Create the instance in Enchantment::staticCtor(), before the loop:
myEnchantment = new MyEnchantment(64, FREQ_UNCOMMON);
  1. Include the header in net.minecraft.world.item.enchantment.h:
#include "MyEnchantment.h"
  1. Add to the build in cmake/Sources.cmake

The loop at the end of staticCtor() will automatically pick up your enchantment and add it to validEnchantments.

The EnchantItemCommand lets you apply enchantments via command. It calls item->enchant() directly, so any registered enchantment works with it automatically. No extra wiring needed.

The AddEnchantmentRuleDefinition class lets game rules apply enchantments to items. It reads an enchantmentId and enchantmentLevel from the rule definition and applies them. It does cap the level at getMaxLevel() for the enchantment, so keep that in mind if you’re relying on game rules to apply enchantments above the normal max.

The vanilla enchantment system only has two built-in “effect channels”: damage bonus (getDamageBonus()) and damage protection (getDamageProtection()). Everything else is handled by reading the enchantment level directly and hooking into specific game code.

So if you want your enchantment to do something new (not just more damage or more protection), you need to:

  1. Create the enchantment class (for registration, costs, and compatibility)
  2. Add a helper method to EnchantmentHelper to read the level
  3. Hook into the relevant game code to use that level

Every non-damage/non-protection enchantment follows this pattern:

Step 1: Enchantment class (just for registration and table appearance)

class MyEnchantment : public Enchantment
{
public:
MyEnchantment(int id, int frequency)
: Enchantment(id, frequency, EnchantmentCategory::weapon)
{
setDescriptionId(IDS_ENCHANTMENT_MY_ENCH);
}
int getMinCost(int level) { return 10 + (level - 1) * 15; }
int getMaxCost(int level) { return getMinCost(level) + 30; }
int getMaxLevel() { return 3; }
};

Step 2: Helper method to read the level from items

EnchantmentHelper.h
static int getMyEnchantmentLevel(shared_ptr<Inventory> inventory);
// EnchantmentHelper.cpp
int EnchantmentHelper::getMyEnchantmentLevel(
shared_ptr<Inventory> inventory)
{
return getEnchantmentLevel(
Enchantment::myEnchantment->id,
inventory->getSelected());
}

Step 3: Hook into gameplay wherever the effect should happen

This is the creative part. Here are some example hooks:

What you wantWhere to hook
Heal on hitMob::doHurtTarget() or the attack code after damage is dealt
Freeze water on walkPlayer movement tick, after position updates
Double dropsThe tile’s spawnResources() method
Speed boostMob::getWalkingSpeedModifier()
Extra XPThe XP orb spawning code in Mob::dropExperience()
Fire immunityMob::hurt() damage type check
Teleport on hitAfter the attack lands in the combat code
Auto-smelt dropsThe tile’s getDrops() or spawnResources() method

Use the right lookup method:

  • Held item: getEnchantmentLevel(id, inventory->getSelected())
  • Single armor piece: getEnchantmentLevel(id, armorItem)
  • Best level across all armor: getEnchantmentLevel(id, inventory->armor) (the array version picks the highest)
  • Any equipment slot: getEnchantmentLevel(id, mob->getEquipmentSlots())

A weapon enchantment that heals the attacker when they deal damage:

LifestealEnchantment.h

#pragma once
#include "Enchantment.h"
class LifestealEnchantment : public Enchantment
{
public:
LifestealEnchantment(int id, int frequency);
virtual int getMinCost(int level);
virtual int getMaxCost(int level);
virtual int getMaxLevel();
virtual bool isCompatibleWith(Enchantment *other) const;
};

LifestealEnchantment.cpp

#include "stdafx.h"
#include "LifestealEnchantment.h"
LifestealEnchantment::LifestealEnchantment(int id, int frequency)
: Enchantment(id, frequency, EnchantmentCategory::weapon)
{
setDescriptionId(IDS_ENCHANTMENT_LIFESTEAL);
}
int LifestealEnchantment::getMinCost(int level)
{
return 10 + (level - 1) * 15;
}
int LifestealEnchantment::getMaxCost(int level)
{
return getMinCost(level) + 30;
}
int LifestealEnchantment::getMaxLevel()
{
return 3;
}
bool LifestealEnchantment::isCompatibleWith(Enchantment *other) const
{
return Enchantment::isCompatibleWith(other);
}

Then add a helper method so the combat code can check for it:

EnchantmentHelper.h
static int getLifestealLevel(shared_ptr<Inventory> inventory);
// EnchantmentHelper.cpp
int EnchantmentHelper::getLifestealLevel(
shared_ptr<Inventory> inventory)
{
return getEnchantmentLevel(
Enchantment::lifesteal->id,
inventory->getSelected());
}

And hook it into the attack code where damage is dealt. You’d heal the player by level health after a successful hit.

Example 2: Frost Walker (Boots Enchantment)

Section titled “Example 2: Frost Walker (Boots Enchantment)”

A boots enchantment that freezes water when you walk over it:

FrostWalkerEnchantment.h

#pragma once
#include "Enchantment.h"
class FrostWalkerEnchantment : public Enchantment
{
public:
FrostWalkerEnchantment(int id, int frequency);
virtual int getMinCost(int level);
virtual int getMaxCost(int level);
virtual int getMaxLevel();
virtual bool isCompatibleWith(Enchantment *other) const;
};

FrostWalkerEnchantment.cpp

#include "stdafx.h"
#include "FrostWalkerEnchantment.h"
FrostWalkerEnchantment::FrostWalkerEnchantment(int id, int frequency)
: Enchantment(id, frequency, EnchantmentCategory::armor_feet)
{
setDescriptionId(IDS_ENCHANTMENT_FROST_WALKER);
}
int FrostWalkerEnchantment::getMinCost(int level)
{
return 10 + (level - 1) * 10;
}
int FrostWalkerEnchantment::getMaxCost(int level)
{
return getMinCost(level) + 15;
}
int FrostWalkerEnchantment::getMaxLevel()
{
return 2;
}
bool FrostWalkerEnchantment::isCompatibleWith(Enchantment *other) const
{
// Incompatible with Feather Falling
if (other->id == Enchantment::fallProtection->id) return false;
return Enchantment::isCompatibleWith(other);
}

Register it at ID 8 (first unused armor slot):

// In Enchantment::staticCtor()
frostWalker = new FrostWalkerEnchantment(8, FREQ_RARE);

The actual water-freezing logic would go in the player’s movement tick. Check for the enchantment on the boots, then scan nearby water blocks and replace them with ice. The radius could scale with the level (2 + level blocks).

Example 3: Making Sharpness Go to Level 10

Section titled “Example 3: Making Sharpness Go to Level 10”

No new class needed. Just change DamageEnchantment::getMaxLevel():

int DamageEnchantment::getMaxLevel()
{
return 10; // Was 5
}

And adjust the cost curve so higher levels are reachable:

int DamageEnchantment::getMinCost(int level)
{
return minCost[type] + (level - 1) * 6; // Tighter spacing (was 11)
}

The damage bonus scales automatically since getDamageBonus() uses the level directly in its formula. Sharpness X would give floor(10 * 2.75) = 27 max bonus damage.

A tool enchantment that smelts blocks as you mine them. This one shows the full pattern for a non-combat enchantment:

Registration:

Enchantment.h
static Enchantment *autoSmelt;
// Enchantment.cpp
Enchantment *Enchantment::autoSmelt = NULL;
// In staticCtor():
autoSmelt = new AutoSmeltEnchantment(36, FREQ_RARE);

The enchantment class is minimal (just costs and compatibility):

class AutoSmeltEnchantment : public Enchantment
{
public:
AutoSmeltEnchantment(int id, int freq)
: Enchantment(id, freq, EnchantmentCategory::digger) {
setDescriptionId(IDS_ENCHANTMENT_AUTO_SMELT);
}
int getMinCost(int level) { return 15; }
int getMaxCost(int level) { return 65; }
int getMaxLevel() { return 1; }
bool isCompatibleWith(Enchantment *other) const {
// Incompatible with Silk Touch
if (other->id == untouching->id) return false;
return Enchantment::isCompatibleWith(other);
}
};

The helper method:

static bool hasAutoSmelt(shared_ptr<Inventory> inventory) {
return getEnchantmentLevel(Enchantment::autoSmelt->id,
inventory->getSelected()) > 0;
}

The gameplay hook goes in the tile mining code, where you’d check hasAutoSmelt() and replace drops with their smelted versions.