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

Adding Enchantments

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

This guide covers the LCE enchantment system: how to create enchantment subclasses, register them, define cost curves, implement damage/protection modifiers, set up compatibility rules, and how the enchanting table’s selection algorithm works. It also covers the anvil combining system from RepairMenu.

For advanced enchantment tricks (conflict overrides, pushing past level limits, enchantments that do completely new things), see Custom Enchantments.

The enchantment system spans several files in Minecraft.World/:

FilePurpose
Enchantment.h / .cppBase class, static registry, frequency constants
EnchantmentCategory.hItem type categories (armor, weapon, digger, bow)
EnchantmentInstance.hPairs an enchantment with a level (used in selection)
EnchantmentHelper.h / .cppStatic utilities: damage calculation, enchanting table algorithm
EnchantmentMenu.h / .cppThe enchanting table container/UI
RepairMenu.h / .cppThe anvil container: combining, renaming, enchantment merging
SubclassesProtectionEnchantment, DamageEnchantment, ThornsEnchantment, etc.
#pragma once
#include "Enchantment.h"
class MyEnchantment : public Enchantment
{
public:
MyEnchantment(int id, int frequency);
virtual int getMinCost(int level);
virtual int getMaxCost(int level);
virtual int getMaxLevel();
// Override one or both of these for combat enchantments:
virtual int getDamageProtection(int level, DamageSource *source);
virtual int getDamageBonus(int level, shared_ptr<Mob> target);
// Override for custom compatibility rules:
virtual bool isCompatibleWith(Enchantment *other) const;
};

Implementation (Minecraft.World/MyEnchantment.cpp)

Section titled “Implementation (Minecraft.World/MyEnchantment.cpp)”
#include "MyEnchantment.h"
MyEnchantment::MyEnchantment(int id, int frequency)
: Enchantment(id, frequency, EnchantmentCategory::weapon)
{
setDescriptionId(IDS_ENCHANTMENT_MY_ENCH);
}
int MyEnchantment::getMinCost(int level)
{
return 5 + (level - 1) * 10;
}
int MyEnchantment::getMaxCost(int level)
{
return getMinCost(level) + 20;
}
int MyEnchantment::getMaxLevel()
{
return 3;
}
int MyEnchantment::getDamageBonus(int level, shared_ptr<Mob> target)
{
// Example: flat bonus per level
return level * 2;
}
bool MyEnchantment::isCompatibleWith(Enchantment *other) const
{
// Incompatible with other damage enchantments
return dynamic_cast<DamageEnchantment *>(other) == NULL;
}

The Enchantment constructor automatically registers itself into the global enchantments[256] array:

Enchantment::Enchantment(int id, int frequency,
const EnchantmentCategory *category)
: id(id), frequency(frequency), category(category)
{
_init(id); // Stores 'this' in enchantments[id]
}

If you use a duplicate ID, it triggers __debugbreak() in debug builds.

MethodDefaultPurpose
getMinLevel()1Minimum enchantment level
getMaxLevel()1Maximum enchantment level
getMinCost(level)1 + level * 10Minimum enchantment value for this level to show up
getMaxCost(level)getMinCost(level) + 5Maximum enchantment value for this level
getFrequency()Constructor argWeight in random selection
getDamageProtection(level, source)0Damage reduction points
getDamageBonus(level, target)0Extra damage dealt
isCompatibleWith(other)this != otherCan coexist on the same item
canEnchant(item)Delegates to categoryWhether this enchantment applies to the item

Frequency controls how likely the enchantment is to be picked from the candidate pool (higher = more common):

ConstantValueExamples
FREQ_COMMON10Protection, Sharpness, Efficiency, Power
FREQ_UNCOMMON5Fire Protection, Smite, Knockback, Unbreaking
FREQ_RARE2Blast Protection, Fire Aspect, Looting, Fortune, Flame, Punch
FREQ_VERY_RARE1Silk Touch, Thorns, Infinity

EnchantmentCategory decides which items an enchantment can go on:

CategoryConstantItems
All itemsEnchantmentCategory::allEverything
Any armorEnchantmentCategory::armorHelmets, chestplates, leggings, boots
Boots onlyEnchantmentCategory::armor_feetBoots
Leggings onlyEnchantmentCategory::armor_legsLeggings
Chestplate onlyEnchantmentCategory::armor_torsoChestplates
Helmet onlyEnchantmentCategory::armor_headHelmets
WeaponsEnchantmentCategory::weaponSwords (and axes via override)
DiggersEnchantmentCategory::diggerPickaxes, shovels, axes
BowsEnchantmentCategory::bowBows

The category check happens through EnchantmentCategory::canEnchant(Item *item), which uses a dynamic_cast chain 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;
}

Some enchantments override canEnchant to work on items outside their category. For example, DamageEnchantment (Sharpness/Smite/Bane) also works on axes:

bool DamageEnchantment::canEnchant(shared_ptr<ItemInstance> item)
{
HatchetItem *hatchet = dynamic_cast<HatchetItem *>(item->getItem());
if (hatchet) return true;
return Enchantment::canEnchant(item);
}

Similarly, ThornsEnchantment has category armor_torso but overrides canEnchant to accept any ArmorItem:

bool ThornsEnchantment::canEnchant(shared_ptr<ItemInstance> item)
{
ArmorItem *armor = dynamic_cast<ArmorItem *>(item->getItem());
if (armor) return true;
return Enchantment::canEnchant(item);
}

And DiggingEnchantment (Efficiency) accepts shears in addition to digger tools:

bool DiggingEnchantment::canEnchant(shared_ptr<ItemInstance> item)
{
ShearsItem *shears = dynamic_cast<ShearsItem *>(item->getItem());
if (shears) return true;
return Enchantment::canEnchant(item);
}

Add your enchantment to Enchantment::staticCtor() in Minecraft.World/Enchantment.cpp:

// 1. Declare in Enchantment.h:
static Enchantment *myEnchantment;
// 2. Initialize in Enchantment.cpp (top of file):
Enchantment *Enchantment::myEnchantment = NULL;
// 3. Create in staticCtor():
void Enchantment::staticCtor()
{
// ... existing enchantments ...
myEnchantment = new MyEnchantment(64, FREQ_UNCOMMON);
// The loop at the end collects all non-null entries
// into validEnchantments automatically:
for (unsigned int i = 0; i < 256; ++i)
{
Enchantment *enchantment = enchantments[i];
if (enchantment != NULL)
{
validEnchantments.push_back(enchantment);
}
}
}

Vanilla enchantments use these ID ranges:

RangeCategory
0-7Armor enchantments
16-21Weapon enchantments
32-35Tool/digger enchantments
48-51Bow enchantments

Pick an ID that doesn’t collide with existing enchantments. IDs 0-255 are valid. Gaps between ranges (8-15, 22-31, 36-47, 52-63, 64-255) are all open.

The isCompatibleWith() method controls which enchantments can live on the same item. This check gets used in two places: the enchanting table algorithm (when picking bonus enchantments) and the anvil (when combining items).

bool Enchantment::isCompatibleWith(Enchantment *other) const
{
return this != other; // Only incompatible with itself
}

ProtectionEnchantment has a more specific rule. Protection types are mutually exclusive (you can’t have both Protection and Fire Protection), but Feather Falling works with all other protection types:

bool ProtectionEnchantment::isCompatibleWith(Enchantment *other) const
{
ProtectionEnchantment *pe =
dynamic_cast<ProtectionEnchantment *>(other);
if (pe != NULL)
{
if (pe->type == this->type) return false;
if (this->type == FALL || pe->type == FALL) return true;
return false;
}
return Enchantment::isCompatibleWith(other);
}

All damage enchantments (Sharpness, Smite, Bane of Arthropods) are mutually exclusive:

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

These two are also mutually exclusive, but through a different mechanism. Each one checks the other’s ID directly:

// UntouchingEnchantment (Silk Touch)
bool UntouchingEnchantment::isCompatibleWith(Enchantment *other) const
{
return other->id != Enchantment::lootBonusDigger->id; // Not compatible with Fortune
}
// LootBonusEnchantment (Fortune/Looting)
bool LootBonusEnchantment::isCompatibleWith(Enchantment *other) const
{
return other->id != Enchantment::untouching->id; // Not compatible with Silk Touch
}

getDamageProtection() returns a protection value that feeds into EnchantmentHelper::getDamageProtection(). The total across all armor pieces is capped at 25, then randomized:

// From EnchantmentHelper.cpp:
// Sum all protection values from armor pieces
if (sum > 25) sum = 25;
return ((sum + 1) >> 1) + random.nextInt((sum >> 1) + 1);

Here’s how ProtectionEnchantment calculates protection based on type:

int ProtectionEnchantment::getDamageProtection(
int level, DamageSource *source)
{
if (source->isBypassInvul()) return 0;
float protect = (6 + level * level) / 3.0f;
if (type == ALL) return floor(protect * 0.75f);
if (type == FIRE) return floor(protect * 1.25f); // fire only
if (type == FALL) return floor(protect * 2.5f); // fall only
if (type == EXPLOSION) return floor(protect * 1.5f); // explosion only
if (type == PROJECTILE)return floor(protect * 1.5f); // projectile only
return 0;
}

The protection values per level work out to:

LevelBase (6+l*l)/3ALL (x0.75)FIRE (x1.25)FALL (x2.5)EXPLOSION (x1.5)PROJECTILE (x1.5)
12.3312533
23.3324855
35.00361277
47.3359181111

Remember, the total across all armor pieces is capped at 25 before the randomization step. A full set of Protection IV gives 20 raw points against all damage types.

getDamageBonus() returns extra damage dealt to a target. The DamageEnchantment class checks the target’s MobType:

int DamageEnchantment::getDamageBonus(
int level, shared_ptr<Mob> target)
{
if (type == ALL)
return floor(level * 2.75f); // Sharpness
if (type == UNDEAD && target->getMobType() == UNDEAD)
return floor(level * 4.5f); // Smite
if (type == ARTHROPODS && target->getMobType() == ARTHROPOD)
return floor(level * 4.5f); // Bane of Arthropods
return 0;
}
LevelSharpness (ALL, x2.75)Smite/Bane (x4.5, matching target)
124
259
3813
41118
51322

The total bonus from all enchantments on the weapon gets randomized in EnchantmentHelper::getDamageBonus():

if (sum > 0)
{
return 1 + random.nextInt(sum);
}
return 0;

So the actual bonus is random between 1 and the calculated sum. Sharpness V gives between 1 and 13 extra damage per hit.

ThornsEnchantment doesn’t use getDamageProtection or getDamageBonus. Instead, it has static helper methods that are called from the damage pipeline:

// 15% chance per level to reflect damage
bool ThornsEnchantment::shouldHit(int level, Random *random)
{
if (level <= 0) return false;
return random->nextFloat() < 0.15f * level;
}
// Reflected damage: 1-4 random, or (level - 10) if level > 10
int ThornsEnchantment::getDamage(int level, Random *random)
{
if (level > 10) return level - 10;
return 1 + random->nextInt(4);
}

When Thorns triggers, it costs 3 durability on the armor piece. When it doesn’t trigger but the piece has Thorns, it costs 1 durability. This is handled by doThornsAfterAttack().

LevelTrigger chanceReflected damage
115%1-4 random
230%1-4 random
345%1-4 random

To make your enchantment actually do something, you need to connect it with EnchantmentHelper. The helper uses an iteration pattern that walks through all enchantments on an item:

// EnchantmentHelper iterates enchantments on items via:
void runIterationOnItem(EnchantmentIterationMethod &method,
shared_ptr<ItemInstance> piece);
void runIterationOnInventory(EnchantmentIterationMethod &method,
ItemInstanceArray inventory);

For protection and damage enchantments, the base class getDamageProtection() and getDamageBonus() methods get called automatically by the existing iteration methods. You don’t need any extra wiring.

For special effects (like Thorns), add a helper function in EnchantmentHelper:

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

Then call this from the relevant game logic (like Mob::hurt(), Mob::doHurtTarget(), etc.).

Understanding how the enchanting table picks enchantments is important for tuning your enchantment’s cost curves. The algorithm has three stages: calculate the enchantment value for each slot, pick which enchantments apply, and apply them to the item.

EnchantmentMenu::slotsChanged() scans a 5x5 area (2 blocks out) around the enchanting table to count bookshelves. The scan checks two layers (table height and one above):

for (int dx = -1; dx <= 1; dx++)
{
for (int dz = -1; dz <= 1; dz++)
{
if ((dx != 0 || dz != 0)
&& level->isEmptyBlock(tableX + dx, tableY, tableZ + dz)
&& level->isEmptyBlock(tableX + dx, tableY + 1, tableZ + dz))
{
// Check for bookshelves at distance 2
if (level->getBlock(tableX + dx * 2, tableY, tableZ + dz * 2)
== Block::bookshelf) bookcases++;
if (level->getBlock(tableX + dx * 2, tableY + 1, tableZ + dz * 2)
== Block::bookshelf) bookcases++;
// ... corner checks too
}
}
}

The key detail: there must be an air block between the table and the bookshelf. If you put a torch or carpet between them, it blocks that bookshelf from counting. The maximum useful count is 15.

EnchantmentHelper::getEnchantmentCost() figures out the enchantment value for each of the three table slots:

int getEnchantmentCost(Random *random, int slot,
int bookcases, shared_ptr<ItemInstance> item)
{
int itemValue = item->getItem()->getEnchantmentValue();
if (itemValue <= 0) return 0; // Not enchantable
if (bookcases > 15) bookcases = 15; // Cap at 15
int selected = random->nextInt(8) + 1
+ (bookcases >> 1)
+ random->nextInt(bookcases + 1);
if (slot == 0) return max(selected / 3, 1); // Top slot (cheapest)
if (slot == 1) return max(selected, bookcases * 2); // Middle slot
return selected; // Bottom slot (raw)
}

Some concrete numbers for the bottom slot (slot 2):

BookshelvesMinimumMaximumAverage
029~5
5416~10
10724~15
15931~20

The middle slot (slot 1) has a guaranteed minimum of bookcases * 2, so with 15 bookshelves it’s always at least 30. The top slot divides by 3 and floors at 1, giving the cheapest option.

When you enchant a book, EnchantmentMenu::clickMenuButton() forces the result to have exactly one enchantment instead of potentially multiple:

if (dynamic_cast<BookItem *>(item->getItem()) != NULL)
{
// Books get a single random enchantment from validEnchantments
// at a random valid level
}

This means enchanted books from the enchanting table always have exactly one enchantment.

EnchantmentHelper::selectEnchantment() picks which enchantments and levels to apply:

  1. Calculate item bonus: Take the item’s getEnchantmentValue(), halve it, add 1, then add two random rolls within that range. This produces a number between 2 and enchantmentValue + 1.
  2. Combine with cost: enchantmentValue = itemBonus + enchantmentCost
  3. Apply deviation: Random +/-15% adjustment. Specifically: value += round(value * (random_float * 2 - 1) * 0.15).
  4. Find candidates: For each registered enchantment, loop through all its valid levels (1 through getMaxLevel()). If enchantmentValue falls within [getMinCost(level), getMaxCost(level)], that enchantment at that level is a valid candidate.
  5. Weighted selection: Pick the first enchantment from candidates using WeighedRandom (by getFrequency()).
  6. Bonus enchantments: Loop with random.nextInt(50) <= bonusChance, halving bonusChance each time. Each iteration removes candidates that are incompatible with already-selected enchantments (using isCompatibleWith()) and picks another weighted random enchantment from what’s left.
// Simplified flow:
int bonusChance = enchantmentValue / 2;
while (random->nextInt(50) <= bonusChance)
{
// Remove incompatible enchantments from candidates
// Pick another random enchantment
bonusChance >>= 1; // Halve the chance each round
}

The bonus enchantment loop is how you get multiple enchantments on one item. With higher enchantment values, the initial bonusChance is higher, making multi-enchantment results more likely.

EnchantmentHelper::enchantItem() applies the selected enchantments. Enchantments are stored in the item’s NBT data under the "ench" tag (or "StoredEnchantments" for enchanted books):

// Each enchantment is a compound tag with:
tag->putShort(TAG_ENCH_ID, enchantment->id);
tag->putShort(TAG_ENCH_LEVEL, level);

These are the getMinCost(level) and getMaxCost(level) values for each vanilla enchantment. The enchanting table picks your enchantment when the calculated enchantment value falls within the min-max range for a given level.

All five protection types use array-based cost curves. The arrays are indexed by the protection type:

ProtectionEnchantment.cpp
static int minCost[] = {1, 10, 5, 5, 3}; // ALL, FIRE, FALL, EXPLOSION, PROJECTILE
static int levelCost[] = {11, 8, 6, 8, 6};
static int levelCostSpan[] = {20, 12, 10, 12, 15};

Formula: minCost = base + (level - 1) * levelCost, maxCost = minCost + levelCostSpan.

EnchantmentLevel 1Level 2Level 3Level 4
Protection1-2112-3223-4334-54
Fire Protection10-2218-3026-3834-46
Feather Falling5-1511-2117-2723-33
Blast Protection5-1713-2521-3329-41
Projectile Protection3-189-2415-3021-36

Also array-based, indexed by damage type:

DamageEnchantment.cpp
static int minCost[] = {1, 5, 5}; // ALL, UNDEAD, ARTHROPODS
static int levelCost[] = {11, 8, 8};
static int levelCostSpan[] = {20, 20, 20};
EnchantmentLevel 1Level 2Level 3Level 4Level 5
Sharpness1-2112-3223-4334-5445-65
Smite5-2513-3321-4129-4937-57
Bane of Arthropods5-2513-3321-4129-4937-57

Each bow enchantment has its own simple formula:

EnchantmentMax LevelMin Cost FormulaMax Cost Formula
Power51 + (level-1) * 10minCost + 15
Punch212 + (level-1) * 20minCost + 25
Flame12050
Infinity12050
EnchantmentLevel 1Level 2Level 3Level 4Level 5
Power1-1611-2621-3631-4641-56
Punch12-3732-57
Flame20-50
Infinity20-50
EnchantmentMax LevelMin Cost FormulaMax Cost Formula
Knockback25 + 20 * (level-1)minCost + 50
Fire Aspect210 + 20 * (level-1)minCost + 50
Looting315 + (level-1) * 9minCost + 50

Note: Knockback, Fire Aspect, and Looting all use Enchantment::getMinCost() (the base class default: 1 + level * 10) for computing maxCost, not their own overridden getMinCost(). This is a quirk of the code. The maxCost formula getMinCost(level) + 50 calls the base class version, giving (1 + level * 10) + 50.

EnchantmentLevel 1Level 2Level 3
Knockback5-6125-71
Fire Aspect10-6130-71
Looting15-6124-7133-81
EnchantmentMax LevelMin Cost FormulaMax Cost Formula
Efficiency51 + 10 * (level-1)minCost + 50
Silk Touch115minCost + 50
Unbreaking35 + (level-1) * 8minCost + 50
Fortune315 + (level-1) * 9minCost + 50

Same quirk as weapon enchantments: Efficiency and Silk Touch use the base class getMinCost() for maxCost. Unbreaking uses its own getMinCost() for maxCost.

EnchantmentLevel 1Level 2Level 3Level 4Level 5
Efficiency1-6111-7121-8131-9141-101
Silk Touch15-65
Unbreaking5-5513-6321-71
Fortune15-6124-7133-81
EnchantmentMax LevelMin Cost FormulaMax Cost Formula
Respiration310 * levelminCost + 30
Aqua Affinity11minCost + 40
Thorns310 + 20 * (level-1)minCost + 50
EnchantmentLevel 1Level 2Level 3
Respiration10-4020-5030-60
Aqua Affinity1-41
Thorns10-7130-9150-111

The anvil uses RepairMenu::createResult() to figure out what happens when you combine two items or apply an enchanted book.

When you put two items in the anvil, the code walks through each enchantment on the second item (the “sacrifice”) and tries to merge it onto the first item (the “target”):

  1. Check compatibility: For each enchantment on the sacrifice, check if it’s compatible with all enchantments already on the target (using isCompatibleWith()). If not, it gets skipped and adds 1 to the cost as a penalty.
  2. Same enchantment merge: If both items have the same enchantment:
    • Same level: the result is level + 1 (capped at getMaxLevel())
    • Different levels: take the higher one
  3. New enchantment: If the target doesn’t have this enchantment, it gets added at the sacrifice’s level.
// Simplified from RepairMenu::createResult()
for each enchantment on sacrifice:
if (targetHasEnchantment(ench.id))
if (target.level == sacrifice.level)
newLevel = min(target.level + 1, ench.getMaxLevel());
else
newLevel = max(target.level, sacrifice.level);
else
newLevel = sacrifice.level;
// Check compatibility with all other enchantments on target
for each other enchantment on target:
if (!ench.isCompatibleWith(other))
skip this enchantment, price += 1;

The anvil cost has two parts: price (the base cost) and tax (the prior work penalty).

Price is built up from:

  • Each enchantment that gets applied adds a fee based on rarity:
RarityFee (normal item)Fee (from book)
Common (freq 10)1 per level1 per level
Uncommon (freq 5)2 per level1 per level
Rare (freq 2)4 per level2 per level
Very Rare (freq 1)8 per level4 per level

The rarity fee is derived from the enchantment’s frequency: FREQ_COMMON maps to 1, FREQ_UNCOMMON to 2, FREQ_RARE to 4, FREQ_VERY_RARE to 8. When applying from an enchanted book, these fees are halved.

  • Renaming adds 1 to the price
  • If the target item is damaged and the sacrifice can repair it, the repair cost is added

Tax is the prior work penalty. Each time an item goes through the anvil, a RepairCost NBT tag gets incremented. The formula is tax = 2^repairCost - 1. So the first anvil use costs 0 extra, the second costs 1, the third costs 3, the fourth costs 7, etc.

The total cost is price + leftTax + rightTax. If this exceeds 39 (the 40-level cap), the anvil says “Too Expensive!” and refuses to combine.

// From RepairMenu::createResult()
if (price + tax > 39)
{
// "Too Expensive!" -- operation not allowed
resultSlots[0] = nullptr;
return;
}

When the sacrifice item is an enchanted book (instead of a tool/weapon/armor), the rarity fees are halved. This is why books are the preferred way to apply expensive enchantments like Silk Touch or Infinity.

IDNameClassCategoryFreqMax Level
0ProtectionProtectionEnchantment (ALL)armor104
1Fire ProtectionProtectionEnchantment (FIRE)armor54
2Feather FallingProtectionEnchantment (FALL)armor_feet54
3Blast ProtectionProtectionEnchantment (EXPLOSION)armor24
4Projectile ProtectionProtectionEnchantment (PROJECTILE)armor54
5RespirationOxygenEnchantmentarmor_head23
6Aqua AffinityWaterWorkerEnchantmentarmor_head21
7ThornsThornsEnchantmentarmor_torso13
IDNameClassCategoryFreqMax Level
16SharpnessDamageEnchantment (ALL)weapon105
17SmiteDamageEnchantment (UNDEAD)weapon55
18Bane of ArthropodsDamageEnchantment (ARTHROPODS)weapon55
19KnockbackKnockbackEnchantmentweapon52
20Fire AspectFireAspectEnchantmentweapon22
21LootingLootBonusEnchantmentweapon23
IDNameClassCategoryFreqMax Level
32EfficiencyDiggingEnchantmentdigger105
33Silk TouchUntouchingEnchantmentdigger11
34UnbreakingDigDurabilityEnchantmentdigger53
35FortuneLootBonusEnchantmentdigger23
IDNameClassCategoryFreqMax Level
48PowerArrowDamageEnchantmentbow105
49PunchArrowKnockbackEnchantmentbow22
50FlameArrowFireEnchantmentbow21
51InfinityArrowInfiniteEnchantmentbow11

The game can display enchantment levels up to 10 using Enchantment::getLevelString(). The levels map to localized strings:

LevelString ID
1IDS_ENCHANTMENT_LEVEL_1 (“I”)
2IDS_ENCHANTMENT_LEVEL_2 (“II”)
3IDS_ENCHANTMENT_LEVEL_3 (“III”)
4IDS_ENCHANTMENT_LEVEL_4 (“IV”)
5IDS_ENCHANTMENT_LEVEL_5 (“V”)
6-10IDS_ENCHANTMENT_LEVEL_6 through IDS_ENCHANTMENT_LEVEL_10

If you make an enchantment with getMaxLevel() higher than 10, the level display won’t have a name for it. It’ll just show the numeric value.