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

Template: Enchantment & Potion

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

This template walks you through adding two new gameplay features from scratch: a Vampiric sword enchantment that heals you when you hit something, and a Levitation potion effect that makes entities float upward. By the end, both will be fully registered, brewable, and showing up on the enchanting table.

If you haven’t set up your build environment yet, start with Getting Started first. This tutorial assumes you can already compile and run the game.

FeatureWhat it does
Vampiric (enchantment)Sword enchantment. Heals the attacker for 1 HP per level when they deal melee damage. Conflicts with Knockback. Shows up on the enchanting table and in loot.
Levitation (potion effect)Status effect. Makes entities float upward at a speed that scales with the amplifier. Brewable from Awkward Potion + Phantom Membrane (or any item you pick). Supports extended duration via Redstone.

We need an unused enchantment ID. Vanilla weapon enchantments use IDs 16-21, so we’ll grab 22 (the first open slot after Looting). For a refresher on the full ID map, see Custom Enchantments.

Here’s what we want:

  • Category: weapon (swords only)
  • Frequency: FREQ_RARE (2) so it feels special but not impossible to find
  • Max level: 3
  • Conflict: Incompatible with Knockback (you shouldn’t be knocking enemies away if you’re trying to lifesteal off them)

Create Minecraft.World/VampiricEnchantment.h:

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

Nothing fancy here. We extend Enchantment and override the methods that control cost curves, max level, and compatibility. We don’t need getDamageBonus() or getDamageProtection() because our effect (healing) doesn’t fit into either of those channels.

Create Minecraft.World/VampiricEnchantment.cpp:

#include "stdafx.h"
#include "VampiricEnchantment.h"
#include "KnockbackEnchantment.h"
VampiricEnchantment::VampiricEnchantment(int id, int frequency)
: Enchantment(id, frequency, EnchantmentCategory::weapon)
{
setDescriptionId(IDS_ENCHANTMENT_VAMPIRIC);
}
int VampiricEnchantment::getMinCost(int level)
{
// Level 1: 15, Level 2: 25, Level 3: 35
return 15 + (level - 1) * 10;
}
int VampiricEnchantment::getMaxCost(int level)
{
return getMinCost(level) + 30;
}
int VampiricEnchantment::getMaxLevel()
{
return 3;
}
bool VampiricEnchantment::isCompatibleWith(Enchantment *other) const
{
// Don't allow Vampiric + Knockback on the same sword
if (other->id == Enchantment::knockback->id)
return false;
return Enchantment::isCompatibleWith(other);
}

Let’s talk about the cost curve. With 15 bookshelves, the enchanting table’s bottom slot generates values roughly between 9 and 31 (plus the item’s enchantment value bonus). Our level 1 range of 15-45 means it starts showing up at mid-level enchantment power. Level 3 needs a value of at least 35, so you’ll mostly see that from high-bookshelf rolls. For more on how cost curves interact with the table, see Adding Enchantments.

Three changes across two files.

In Enchantment.h, add the static pointer with the other enchantment declarations:

// After the existing static pointers (knockback, fireAspect, etc.)
static Enchantment *vampiric;

In Enchantment.cpp, initialize it to NULL at the top of the file:

Enchantment *Enchantment::vampiric = NULL;

Then create it inside staticCtor(), before the loop that populates validEnchantments:

void Enchantment::staticCtor()
{
// ... existing enchantments ...
vampiric = new VampiricEnchantment(22, FREQ_RARE);
// The existing loop picks up all non-null entries:
for (unsigned int i = 0; i < 256; ++i)
{
Enchantment *enchantment = enchantments[i];
if (enchantment != NULL)
{
validEnchantments.push_back(enchantment);
}
}
}

In net.minecraft.world.item.enchantment.h, add the include:

#include "VampiricEnchantment.h"

That’s it for registration. The enchanting table will now consider Vampiric as a candidate for swords, and enchanted books with Vampiric can appear in loot chest rolls because validEnchantments is used by the loot system.

You need to define IDS_ENCHANTMENT_VAMPIRIC in the string table so the tooltip shows “Vampiric” instead of garbage. The exact process depends on your string table setup, but the gist is: add a new entry with the display name “Vampiric” and map the constant to that entry’s ID.

We need a way for the combat code to check if the player’s held weapon has Vampiric and at what level.

In EnchantmentHelper.h:

static int getVampiricLevel(shared_ptr<Inventory> inventory);

In EnchantmentHelper.cpp:

int EnchantmentHelper::getVampiricLevel(
shared_ptr<Inventory> inventory)
{
return getEnchantmentLevel(
Enchantment::vampiric->id,
inventory->getSelected());
}

This follows the same pattern as getKnockbackBonus(), getFireAspect(), and the other weapon enchantment helpers. For background on how getEnchantmentLevel reads the NBT tags, see Custom Enchantments.

Now for the fun part. We need to heal the attacker after they deal damage. The best place to do this is in Mob::doHurtTarget(), right after the damage is applied.

In Mob.cpp, find doHurtTarget(). After the target takes damage successfully, add the heal:

bool Mob::doHurtTarget(shared_ptr<Entity> target)
{
// ... existing damage calculation and application ...
// After the target is hurt and damage was dealt:
int vampiricLevel = EnchantmentHelper::getVampiricLevel(
this->inventory);
if (vampiricLevel > 0)
{
// Heal 1 HP per enchantment level
int healAmount = vampiricLevel;
if (this->getHealth() < this->getMaxHealth())
{
this->heal(healAmount);
}
}
// ... rest of the method (fire aspect, knockback, etc.) ...
return true;
}

The heal is simple and predictable: Vampiric I heals 1 HP (half a heart), Vampiric II heals 2 HP (one heart), Vampiric III heals 3 HP (one and a half hearts). No randomness, no percentage scaling. It’s strong enough to matter in sustained combat without being broken on a single hit.

At this point, Vampiric is fully functional. You can test it with:

  1. Enchanting table: Place a sword on the table with 15 bookshelves. It should occasionally appear as a candidate, especially at higher enchantment costs.
  2. Command: /enchant @p 22 3 gives you Vampiric III directly.
  3. Anvil: Enchanted books with Vampiric can be combined onto swords. The anvil will block combining Vampiric + Knockback because of our compatibility rule.
  4. Loot: Enchanted books in dungeon/temple chests can now roll Vampiric since it’s in validEnchantments.

The MobEffect registry has 32 slots. Vanilla LCEMP uses IDs 1-19, and slots 20-31 are reserved and set to NULL. We’ll use ID 25 (IDs 20-23 are taken in the MC build, so picking 25 keeps us safe if you ever want MC compatibility).

For a full breakdown of the effect registry, see Custom Potions & Brewing.

Our Levitation effect:

  • Harmful: Yes (being forced upward is generally bad)
  • Color: A light blue/white (we’ll use a custom color or borrow one)
  • Behavior: Every tick, push the entity upward. Speed scales with amplifier.
  • Duration modifier: 0.5 (harmful effects default to half duration)

In MobEffect.h, find the reserved slot declaration and rename it:

// Change this:
static MobEffect *reserved_25;
// To this:
static MobEffect *levitation;

In MobEffect.cpp, replace the reserved_25 initialization with your effect:

MobEffect *MobEffect::levitation =
(new MobEffect(25, true, eMinecraftColour_Effect_WaterBreathing))
->setDescriptionId(IDS_POTION_LEVITATION)
->setPostfixDescriptionId(IDS_POTION_LEVITATION_POSTFIX)
->setIcon(MobEffect::e_MobEffectIcon_Jump);

We’re borrowing Water Breathing’s light blue color for the particles. If you want a unique color, add a new eMinecraftColour constant. The icon uses the Jump Boost icon as a placeholder. You can add a custom icon if you have sprite sheet space.

Levitation needs to run every single tick to smoothly push entities upward. We need to touch two methods.

First, in isDurationEffectTick(), add Levitation so it fires every tick:

bool MobEffect::isDurationEffectTick(
int remainingDuration, int amplification)
{
if (id == regeneration->id || id == poison->id)
{
int interval = 25 >> amplification;
if (interval > 0)
return (remainingDuration % interval) == 0;
return true;
}
else if (id == hunger->id)
{
return true;
}
else if (id == levitation->id)
{
return true; // Every tick
}
return false;
}

Then, in applyEffectTick(), add the floating behavior:

void MobEffect::applyEffectTick(
shared_ptr<Mob> mob, int amplification)
{
// ... existing effects (regeneration, poison, hunger, heal, harm) ...
else if (id == levitation->id)
{
// Push the entity upward each tick
// Base speed: 0.05 blocks/tick, scales with amplifier
double liftSpeed = 0.05 * (double)(amplification + 1);
mob->yd += liftSpeed;
// Cancel fall damage accumulation while floating
mob->fallDistance = 0.0f;
}
}

The speed scaling works out to:

LevelAmplifierLift per tickBlocks per second
I00.05~1.0
II10.10~2.0
III20.15~3.0

We also reset fallDistance each tick so the entity doesn’t accumulate fall damage while floating. When the effect wears off, they’ll start falling and fallDistance will build up normally from that point.

If you want Levitation I to reliably make entities float (not just slow their fall), change the base speed:

// Alternative: stronger base lift that clearly overcomes gravity
double liftSpeed = 0.10 * (double)(amplification + 1);

Like with the enchantment, you need string table entries:

  • IDS_POTION_LEVITATION for the effect name (“Levitation”)
  • IDS_POTION_LEVITATION_POSTFIX for the potion name suffix (“of Levitation”)

These show up in the potion tooltip and inventory name.


LCE uses a bitfield system for potions. Every potion is a 15-bit integer where specific bits encode which effect is active, whether it’s extended, amplified, or throwable. Ingredients have formula strings that flip bits. For the full explanation, see Custom Potions & Brewing.

The key bits:

BitsPurpose
0-3Which effect
4Enabler (set by Nether Wart)
5Amplifier (set by Glowstone)
6Duration extension (set by Redstone)
13Functional potion marker
14Splash/throwable

We need an unused combination of bits 0-3. Looking at what’s taken:

Bits 3210EffectTaken?
0111Free
1101Free
1110Free
1111Free

We’ll use 1101 (bits 0, 2, and 3 set, bit 1 clear) for Levitation.

In PotionBrewing.h, add the formula constant:

static const wstring MOD_PHANTOMMEMBRANE;

In PotionBrewing.cpp, define it:

const wstring PotionBrewing::MOD_PHANTOMMEMBRANE =
L"+0-1+2+3&4-4+13";

This sets bits 0, 2, 3 (giving our 1101 pattern), clears bit 1, requires the Awkward Potion enabler bit (4), consumes it, and marks it as functional (bit 13). For a full explanation of the formula syntax, see Custom Potions & Brewing.

In Item.cpp, when the ingredient item is created, chain the formula:

// If using an existing item, find where it's created and add:
Item::phantomMembrane = (new Item(PHANTOM_MEMBRANE_ID))
->setTextureName(L"phantomMembrane")
->setDescriptionId(IDS_ITEM_PHANTOM_MEMBRANE)
->setPotionBrewingFormula(PotionBrewing::MOD_PHANTOMMEMBRANE);

If you’re attaching this to an item that already exists (like a Feather or something), just find that item’s creation line and chain ->setPotionBrewingFormula(PotionBrewing::MOD_PHANTOMMEMBRANE) onto it.

Once an item has setPotionBrewingFormula() called on it, the brewing stand accepts it as an ingredient automatically. No other wiring needed.

3.4 Map the brew value to the Levitation effect

Section titled “3.4 Map the brew value to the Levitation effect”

The brewing system needs to know that our bit pattern (1101 in bits 0-3) should produce the Levitation effect. This happens in PotionBrewing::staticCtor().

In PotionBrewing.cpp, inside staticCtor(), add these entries:

// Duration formula: bits 0, 2, 3 must be set, bit 1 must be clear
// The "0+6" part means: base duration value is bit 0's contribution,
// plus bit 6 if Redstone was added (for extended duration)
potionEffectDuration.insert(intStringMap::value_type(
MobEffect::levitation->getId(),
L"0 & !1 & 2 & 3 & 0+6"
));
// Amplifier formula: bit 5 = Glowstone was added
potionEffectAmplifier.insert(intStringMap::value_type(
MobEffect::levitation->getId(),
L"5"
));

The duration formula checks that our bit pattern (1101) is present, then uses 0+6 for the duration value. Without Redstone (bit 6 clear), duration is short. With Redstone (bit 6 set), it’s longer. For the full formula syntax breakdown, see Custom Potions & Brewing.

With everything in place, here’s what the brewing chain looks like:

StepInputIngredientOutput
1Water BottleNether WartAwkward Potion
2Awkward PotionPhantom MembranePotion of Levitation
3Potion of LevitationRedstonePotion of Levitation (Extended)
4Potion of LevitationGlowstonePotion of Levitation II
5Any Levitation potionGunpowderSplash Potion of Levitation

Steps 3-5 work automatically. Redstone’s formula (-5+6-7) sets the duration extension bit. Glowstone’s formula (+5-6-7) sets the amplifier bit. Gunpowder’s formula (+14) sets the throwable bit. You don’t need to write any extra code for those.

The potion liquid color comes from the MobEffect’s color. We used eMinecraftColour_Effect_WaterBreathing (light blue), so the liquid will be light blue. If you want something different, define a new eMinecraftColour constant and pass it to the constructor.


Because we registered Vampiric with FREQ_RARE and the weapon category, it automatically works in all the right places:

  • Enchanting table: Considered as a candidate for swords. FREQ_RARE (weight 2) makes it about as common as Fire Aspect or Looting.
  • Enchanted book loot: The validEnchantments list (built at the end of staticCtor()) is what the loot system uses. Vampiric books can now appear in dungeon and temple chests.
  • Anvil: Players can apply Vampiric books to swords. The Knockback conflict blocks that combo, and the rarity fee is 4 per level from a sword or 2 per level from a book.
  • /enchant command: Works automatically. /enchant @p 22 3 gives Vampiric III.

In cmake/Sources.cmake, add the new source file:

set(MINECRAFT_WORLD_SOURCES
# ... existing sources ...
Minecraft.World/VampiricEnchantment.cpp
)

Build the project and launch the game. Here’s a quick test checklist:

Vampiric Enchantment:

  • /enchant @p 22 1 applies Vampiric I to a held sword
  • Tooltip shows “Vampiric I” on the sword
  • Hitting a mob heals you by half a heart (1 HP)
  • /enchant @p 22 3 gives Vampiric III, heals 1.5 hearts per hit
  • Vampiric cannot be combined with Knockback via anvil
  • Vampiric appears as a candidate on the enchanting table with 15 bookshelves

Levitation Effect:

  • /effect @p 25 30 0 gives Levitation I for 30 seconds
  • You float upward while the effect is active
  • /effect @p 25 30 1 gives Levitation II (faster float)
  • No fall damage while floating
  • Fall damage starts when the effect wears off and you drop
  • Particles appear around the player (light blue swirls)

Brewing:

  • Phantom Membrane is accepted in the brewing stand ingredient slot
  • Awkward Potion + Phantom Membrane = Potion of Levitation
  • Potion of Levitation + Redstone = Potion of Levitation (Extended)
  • Potion of Levitation + Glowstone = Potion of Levitation II
  • Any Levitation potion + Gunpowder = Splash version
  • Drinking the potion applies the Levitation effect
  • Splash version affects nearby mobs

ProblemLikely causeFix
Enchantment doesn’t appear on the tablegetMinCost(1) is too high for the table’s value range (max ~31 with 15 bookshelves)Lower getMinCost() or widen the cost window
Potion has no effect when drunkisDurationEffectTick() returns false for your effect IDAdd your effect to the isDurationEffectTick() method
Brewing produces a Water BottleFormula failed a & requirement checkTrace the formula manually; usually means you didn’t start from Awkward Potion
Heal doesn’t trigger on hitHeal code is placed before the hurt() call succeedsMove the heal after the point where damage is confirmed
Wrong particle colorBorrowed another effect’s eMinecraftColourDefine a custom color constant