Skip to content

Wizards of Lua — Examples

Here are some small examples to help you get started with Lua scripting in Minecraft using the Wizards of Lua mod.


Create a 3x3 Platform Below You

local pos = spell.owner.pos + Vec3(0, -1, 0)
for x = -1, 1 do
  for z = -1, 1 do
    spell.pos = pos + Vec3(x, 0, z)
    spell.block = Block("slime_block")
  end
end

Delete the Block You're Looking At

local hit = spell.owner:raycastBlock(5)
if not hit then return end
spell.pos = hit.blockPos
spell.block = Block("air") -- deletes the block

local hit = spell.owner:raycastBlock(5)
if not hit then return end
spell.pos = hit.blockPos
print("id", spell.block.type.id)
print("name", spell.block.type.name)
print("data", str(spell.block.data))

Auto-Build Ladder Up a Wall

Builds ladders up the targeted wall until blocked or the backing isn't solid.

local hit = spell.owner:raycastBlock(5)
if not hit or hit.side == "up" or hit.side == "down" then return end
local ladder = Block("ladder")
ladder.data.facing = hit.side
spell.pos = hit.blockPos
spell:move(hit.side)
while true do
  spell.block = ladder
  spell:move("up")
  if spell.block.type.id ~= "air" then break end
  spell:move(hit.side, -1)
  if not spell.block.opaque then break end
  spell:move(hit.side)
end

Create an Ongoing Cloud of Smoke Particles

while true do
  spell.pos = spell.owner.pos
  spell:execute("/particle minecraft:smoke ~ ~2 ~ 1 2 1 0 20 force")
  sleep(20)
end

Tip: Use spell:executeSilent(...) to hide the command output. Use /wol spell break to stop all long running spells.


Make a Spell Visible

spell.pos = spell.owner.eyePos
spell.visible = true
spell.velocity = spell.owner.lookVec * 0.1
sleep(20 * 10)

Change Your Inventory

local player = spell.owner --[[@as Player]]
local slot = player.inventory.selected
player.inventory.slots[slot] = Item("diamond", {count = 16})

Give Players a Diamond Sword on Login

local eventQueue = spell:collect("PlayerJoinedEvent")
while true do
  local event = eventQueue:next() --[[@as PlayerJoinedEvent]]  
  event.player.mainHandItem = Item("diamond_sword")
end

Note: This spell only works in multiplayer; in singleplayer, all spells stop when you leave the world.


Block Protection Zone

Prevents block breaking or placing in a radius:

local radius = 10
spell.visible = true
spell:intercept({ "BeforePlayerBlockBreakEvent", "PlayerUseBlockEvent" }, function(event)
  local pos = type(event) == "BeforePlayerBlockBreakEvent" and event.pos or event.hitResult.blockPos
  if spell.world == event.world then
    return (pos - spell.pos):magnitude() > radius
  end
end)

while true do sleep(20) end

Auto-Toggle Gamemode Field

Switch players between survival and adventure automatically.

local radius = 10
spell.visible = true
local pos = spell.pos

while true do
  sleep(1)
  for _, p in ipairs(spell:findEntities("@a[distance=.." .. radius .. ",gamemode=survival]")) do
    ---@cast p Player
    p.gamemode = "adventure"
    spell.pos = p.pos
    spell:execute("/playsound minecraft:block.enchantment_table.use master @a[distance=..5] ~ ~ ~")
  end
  spell.pos = pos
  for _, p in ipairs(spell:findEntities("@a[distance=" .. radius .. "..,gamemode=adventure]")) do
    ---@cast p Player
    p.gamemode = "survival"
    spell.pos = p.pos
    spell:execute("/playsound minecraft:entity.illusioner.cast_spell master @a[distance=..5] ~ ~ ~")
  end
  spell.pos = pos
end

Smite Nearby Zombies

Strikes all nearby zombies with lightning.

local nearbyZombies = spell:findEntities("@e[type=zombie,distance=..20]")
for i = 1, 3 do
  for _, zombie in ipairs(nearbyZombies) do
    spell.pos = zombie.pos
    spell:execute("/summon lightning_bolt")
  end
  sleep(10)
end

Night Vision for Golden Helmet Wearers

Continuously grants Night Vision to any player wearing a golden helmet and removes it once they take it off.

local tracked = {}
while true do
  for _, player in ipairs(spell:findEntities("@a")) do
    ---@cast player Player
    local head = player:getEquipment("head")
    local wearing = head and head.type.id == "golden_helmet"
    local id = player.name
    if wearing then
      spell:executeSilent(
        "/effect give " .. id
        .. " minecraft:night_vision 20 0 true"
      )
      tracked[id] = true
    elseif tracked[id] then
      spell:executeSilent(
        "/effect clear " .. id .. " minecraft:night_vision"
      )
      tracked[id] = nil
    end
  end
  sleep(20)
end

Thunder Stick: Lightning Bolt Spell

Assigns a special stick to the player. When swung, it strikes nearby hostile mobs with lightning.

local spellName = "thunder-spell"
-- Prevent duplicates by stopping existing spell with same name
spell:executeSilent("/wol spell break byName " .. spellName)
spell.name = spellName

local function castLightningBolt(player)
  local oldPos = spell.pos
  spell.pos = player.pos
  spell.world = player.world
  local nearbyEntities = spell:findEntities("@e[type=!player,distance=..20]")
  local hostileEntities = {}
  for _, entity in ipairs(nearbyEntities) do
    if instanceOf(LivingEntity, entity) then
      ---@cast entity LivingEntity
      if entity.hostile then
        table.insert(hostileEntities, entity)
      end
    end
  end
  if #hostileEntities > 0 then
    spell.pos = player.pos
    spell:execute("/particle minecraft:instant_effect ~ ~1 ~ 0 1 0 2 50 force")
  end
  for i = 1, 3 do
    for _, entity in ipairs(hostileEntities) do
      spell.pos = entity.pos
      spell:execute("/summon lightning_bolt")
    end
    sleep(10)
  end
  spell.pos = oldPos
end

local stick = Item("stick")
stick.name = "Thunder Stick"
local player = spell.owner --[[@as Player]]
player.mainHandItem = stick
local eventQueue = spell:collect("PlayerHandSwingEvent")
while true do
  local event = eventQueue:next() --[[@as PlayerHandSwingEvent]]
  if event.hand == "mainhand" then
    if event.player.mainHandItem and event.player.mainHandItem.name == stick.name then
      local hit = event.player:raycastBlock(5)
      if not hit then
        castLightningBolt(event.player)
      end
    end
  end
end

Move Minecarts Up a Ladder and Launch Them Horizontally

This spell detects minecarts (all types) and moves them upward while they are inside a ladder block. If they reach the top of the ladder, they are launched away from the ladder, in the direction opposite to the ladder's facing. This can be used to build vertical minecart elevators with automatic directional exits at the top.

local name = "vertical-minecart-spell"

-- Prevent duplicates by stopping existing spell with same name
spell:executeSilent("/wol spell break byName " .. name)
spell.name = name

print("Starting Vertical Minecart Spell")

local verticalSpeed = 0.1
local exitSpeed = 0.2

local exitDirection = {
  north = Vec3(0, 0, 1),
  south = Vec3(0, 0, -1),
  west = Vec3(1, 0, 0),
  east = Vec3(-1, 0, 0)
}

local types = { "minecart", "chest_minecart", "furnace_minecart",
 "hopper_minecart", "tnt_minecart" }

local function addAll(t1, t2)
  for k, v in ipairs(t2) do
    table.insert(t1, v)
  end
end

while true do
  -- Collect all types of minecarts
  local minecarts = {}
  for _, t in ipairs(types) do
    addAll(minecarts, spell:findEntities("@e[type=" .. t .. "]"))
  end

  for _, minecart in ipairs(minecarts) do
    local currentPos = minecart.pos
    spell.pos = currentPos

    if spell.block.type.id == "ladder" then
      -- Move minecart upward on ladder
      local velocity = currentPos:floor() + Vec3(0.5, 0, 0.5) - currentPos
      velocity.y = verticalSpeed
      minecart.velocity = velocity
    else
      -- If not on a ladder, check if the block below is a ladder for exit launch
      spell.pos = currentPos + Vec3(0, -0.5, 0)
      if spell.block.type.id == "ladder" then
        minecart.velocity = exitDirection[spell.block.data.facing] * exitSpeed
      end
    end
  end
  sleep(1)
end

Safe Zone: Disable PvP in Area

Creates a circular safe zone (radius = 10 meters) around the spell’s position where players cannot damage each other:

local name = "safezone-spell"

-- Prevent duplicates by stopping existing spell with same name
spell:executeSilent("/wol spell break byName " .. name)
spell.name = name

print("Starting Safezone Spell")
local radius = 10
spell:intercept("PlayerAttackEntityEvent", function(event)
  ---@cast event PlayerAttackEntityEvent
  local attacker = event.player
  local target = event.entity
  if spell.world == event.world then
    if instanceOf(Player, target) then
      local distance = (attacker.pos - spell.pos):magnitude()
      if distance <= radius then
        spell:execute("/title " .. attacker.name .. " actionbar \"§cPvP is disabled here!\"")
        return false -- block the attack
      end
    end
  end
  return true
end)

while true do
  sleep(20)
end

Add Crafting Recipes

Adds recipes to your world: paper and string make a name tag, and three strings make two cobwebs. A recipe is a file in a data pack, and a data pack is a folder in the datapacks folder of the world. A spell's io functions work in the world folder, so it can write these files itself, and /reload loads the new pack without a restart. To add your own recipes, fill out the table at the end:

local function writeJson(path, value)
  local file = assert(io.open(path, "w"))
  file:write(Json:encode(value))
  file:close()
end

-- Minecraft 1.21.1 wants a table for each ingredient
local function toJsonIngredients(ids)
  local result = {}
  for i, id in ipairs(ids) do
    result[i] = spell.server.version == "1.21.1" and { item = id } or id
  end
  return result
end

local function writeDatapack(pack)
  local folder = "datapacks/" .. pack.name
  local recipeFolder = folder .. "/data/" .. pack.namespace .. "/recipe"
  spell.server.filesystem:makeDir(recipeFolder)

  writeJson(folder .. "/pack.mcmeta", {
    pack = {
      description = pack.description,
      pack_format = pack.minFormat,
      supported_formats = { pack.minFormat, pack.maxFormat },
      min_format = pack.minFormat,
      max_format = pack.maxFormat
    }
  })

  for name, recipe in pairs(pack.recipes) do
    writeJson(recipeFolder .. "/" .. name .. ".json", {
      type = "minecraft:crafting_shapeless",
      ingredients = toJsonIngredients(recipe.ingredients),
      result = { id = recipe.result, count = recipe.count or 1 }
    })
  end
end

writeDatapack({
  name = "my-recipes",
  namespace = "my_recipes",
  description = "Recipes of a spell",
  minFormat = 48,  -- Minecraft 1.21.1
  maxFormat = 121, -- Minecraft 26.3
  recipes = {
    name_tag = {
      ingredients = { "minecraft:paper", "minecraft:string" },
      result = "minecraft:name_tag"
    },
    cobweb = {
      ingredients = { "minecraft:string", "minecraft:string", "minecraft:string" },
      result = "minecraft:cobweb",
      count = 2
    }
  }
})

spell:execute("/reload")

The recipes stay in the world after the spell has ended. To remove them, delete the folder datapacks/my-recipes and run /reload. A recipe you take out of the table stays until then, since the spell only writes files and never deletes one.

minFormat and maxFormat say which Minecraft versions the pack is for. A newer version than 26.3 may need a higher maxFormat: the Minecraft Wiki lists the numbers under Pack format, and the other kinds of recipes under Recipe.


Send a Pig on an Errand

Walks a pig to a spot ten blocks north and reports what became of it:

spell.pos = spell.owner.eyePos + spell.owner.lookVec * 5
local pig = spell:summon("pig") --[[@as MobEntity]]
sleep(20) -- let it land, so that the waypoint is on the ground too
local waypoint = pig.pos + Vec3(0, 0, -10)

local ok, reason = pig:startPathTo(waypoint, { speed = 1.2 })
if ok then
  ok, reason = pig:awaitTask() -- sleeps until the pig got there or gave up
end

if ok then
  print("Walked " .. math.floor(pig.task.traveled) .. " blocks in " .. pig.task.age .. " ticks.")
else
  print("Gave up: " .. reason)
end

awaitTask() reports the same way startPathTo() does, so one if handles both a pig that cannot start and one that gives up on the way.


Gather Mobs at a Spot

Registers a dynamic command that sends every mob an entity selector finds to a spot. Each mob walks until it is within the given distance of the spot, and the command reports how many got there. Run this once, for example from your startup spell:

WizardsOfLua.setCommand({
  id      = "gather",
  level   = 2,
  pattern = "gather targets:%entities to spot:%v within distance:%i",
  code    = [[
    local mobs = {}
    for _, entity in ipairs(args.targets) do
      -- A selector can also find players, items and the like; only mobs take tasks.
      if instanceOf(MobEntity, entity) then
        ---@cast entity MobEntity
        if entity:startPathTo(args.spot, { targetRadius = args.distance }) then
          mobs[#mobs + 1] = entity
        end
      end
    end

    local arrived = 0
    for _, mob in ipairs(mobs) do
      if mob:awaitTask() then
        arrived = arrived + 1
      end
    end
    print(arrived .. " of " .. #mobs .. " mobs got there.")
  ]]
})

Then gather all sheep within 30 blocks at a spot ten blocks south of you:

/gather @e[type=sheep,distance=..30] to ~ ~ ~10 within 3

Once a mob has got there, its task is done and it goes back to its own behavior, so the sheep start grazing and wandering again. To keep them there, give each one mob:startStay() after it arrived.


Drop Items from Your Inventory

Registers a dynamic command that drops items from your inventory where you are looking. Its choices list what you carry right now, plus * for everything, so Tab offers exactly those and any other value is rejected. Run this once, for example from your startup spell:

WizardsOfLua.setCommand({
  id = "drop",
  pattern = "drop item:%s",
  level = 0,
  choices = {
    item = [[
      if not instanceOf(Player, spell.owner) then return {} end
      local player = spell.owner
      local items = player.inventory.slots
      local result = {}
      for _, v in pairs(items) do
        if v then
          table.insert(result, v.type.id)
        end
      end
      table.insert(result, "*")
      return result
    ]]
  },
  code = [[
    if not instanceOf(Player, spell.owner) then return end
    local item = args.item
    local player = spell.owner
    local items = player.inventory.slots
    local hit = player:raycastBlock(5)
    local pos = hit and hit.pos or player.eyePos + player.lookVec * 5
    for k, v in pairs(items) do
      if v and (v.type.id == item or item == "*") then
        spell.pos = pos + Vec3(math.random() - 0.5, 0.1, math.random() - 0.5)
        local e = ItemEntity(v)
        e.velocity = Vec3(0, 0.2, 0)
        items[k] = nil
      end
    end
  ]]
})

Then drop all your diamonds, or everything you carry:

/drop diamond
/drop *

Ask New Players to Accept the Rules

Shows a dialog to every player who joins and hasn't agreed to the rules yet. A button in a dialog runs a command as the player, so a dynamic command brings the answer back to the spell, and the player's extra keeps it for the next login. Run this once, for example from your startup spell:

WizardsOfLua.setCommand({
  id = "rules",
  level = 0,
  pattern = "rules accept",
  code = [[
    spell.owner:putExtra({ rulesAccepted = true })
    print("Thank you, and have fun!")
  ]]
})

local dialog = Json:encode({
  type = "minecraft:confirmation",
  title = "The Rules of This Server",
  body = {
    type = "minecraft:plain_message",
    contents = "Be nice to each other, and leave the buildings of others alone."
  },
  yes = {
    label = "I agree",
    action = { type = "minecraft:run_command", command = "/rules accept" }
  },
  no = { label = "Not now" }
})

local queue = spell:collect("PlayerJoinedEvent")
while true do
  local event = queue:next() --[[@as PlayerJoinedEvent]]
  local player = event.player
  sleep(40) -- the player's client is still loading the world
  if not player.extra.rulesAccepted then
    spell:executeSilent("/dialog show " .. player.name .. " " .. dialog)
  end
end

This is a friendly reminder and no lock: a player who chooses "Not now" can play on, and sees the dialog again at the next login. The Minecraft Wiki describes the other kinds of dialogs.

Note: Dialogs need Minecraft 1.21.6 or newer.


Village Patrol

Sends a villager on patrol between two posts at a strolling pace, for as long as you stay nearby. Step away and he returns to his own business — with his job site and his bed intact, because a task only borrows his attention:

local owner = spell.owner
local villager = spell:summon("villager") --[[@as MobEntity]]

spell:move(owner.facing, 16)
local posts = { villager.pos, spell.pos }

local function ownerIsNear()
  return owner.alive and (owner.pos - villager.pos):magnitude() < 32
end

local function waitForTask()
  while villager.busy do
    if not ownerIsNear() then
      villager:stopTask()
      return false
    end
    sleep(10)
    spell.pos = villager.pos -- ensure that the chunk stays loaded
  end
  return true
end

local function patrol()
  while villager.alive and ownerIsNear() do
    for _, post in ipairs(posts) do
      villager.watchTarget = nil -- look where he walks
      local ok, reason = villager:startPathTo(post, { targetRadius = 0, speed = 0.5, searchRange = 200 })
      if not ok then
        print("The villager can't get to his post:", reason)
        return
      end
      if not waitForTask() then return end
      villager:startStay({ duration = 3 * 20 })
      villager.watchTarget = owner
      if not waitForTask() then return end
    end
  end
end

patrol()
if villager.alive then
  villager.watchTarget = nil
  print("The villager goes back to work.")
end

Tarman, the Hit-and-Run Zombie

Summons a zombie ten blocks ahead of you. It charges, runs off as soon as it takes a hit, and comes back once it has caught its breath. The helmet keeps it from burning in daylight, and it swings the axe it is holding - a mob attacks with its weapon and the damage that weapon gives it:

local victim = spell.owner --[[@as Player]]
spell.pos = victim.eyePos + victim.lookVec * 10
local zombie = spell:summon("zombie") --[[@as MobEntity]]
local oldName = zombie.name
zombie.name = "Tarman"
zombie:setEquipment("head", Item("leather_helmet"))
zombie:setEquipment("mainhand", Item("iron_axe"))
local hits = spell:collect("AfterLivingEntityDamageEvent")

while zombie.alive and victim.alive do
  local ok, reason = zombie:startAttack(victim)
  if not ok then
    print("Tarman can't attack you: " .. reason) -- e.g. "invalid_target" in creative mode
    break
  end

  local hurt = false
  while zombie.busy and not hurt do
    local evt = hits:next(10) --[[@as AfterLivingEntityDamageEvent]] 
    hurt = evt ~= nil and evt.entity == zombie
    spell.pos = zombie.pos -- ensure that the chunk stays loaded
  end

  if hurt then
    zombie:startFleeFrom(victim, { distance = 12, speed = 1.4 })
    zombie:awaitTask()
    sleep(3 * 20) -- catch breath
    hits:latest() -- forget the hits taken while running
  end
end

if zombie.alive then
  zombie.name = oldName
  zombie:dropItem(zombie:getEquipment("head"))
  zombie:setEquipment("head", nil)
  zombie:dropItem(zombie:getEquipment("mainhand"))
  zombie:setEquipment("mainhand", nil)
end

Spider Bodyguard

Gives you a spider that stays at your side and attacks whoever hurts you. When the fight is over it returns to you. If you die, it dies with you — and if you stop the spell, it is an ordinary spider again, daylight and all:

local owner = spell.owner
spell.pos = owner.eyePos + owner.lookVec * 3
local spider = spell:summon("spider") --[[@as MobEntity]]
spider.name = "Bodyguard"

local function guard()
  spider:startFollow(owner, { distance = 5, speed = 1.3 })
  spider.watchTarget = owner
end

guard()
local hits = spell:collect("AfterLivingEntityDamageEvent")
while spider.alive and owner.alive do
  local evt = hits:next(20) --[[@as AfterLivingEntityDamageEvent]]  
  if evt and evt.entity == owner then
    local attacker = evt.damageSource.attacker --[[@as LivingEntity?]]
    if attacker and attacker ~= owner and attacker ~= spider then
      spider.watchTarget = nil
      if spider:startAttack(attacker, { searchRange = 30 }) then
        spider:awaitTask()
      end
      guard()
    end
  end
end

if spider.alive then
  spider:kill()
end

A Helper Who Does What You Say

Brings a bot into the game: a player without a client that your spell controls. It appears on the block you look at, or 5 blocks in front of you if there is none, and listens to the chat. Write come, follow, stay or bye, and it does so:

local owner = spell.owner --[[@as Player]]
local hit = owner:raycastBlock(20)
local pos = hit and hit.blockPos + Vec3(0.5, 1, 0.5) or owner.eyePos + owner.lookVec * 5
local bot = Bot:spawn("MickKay", pos)
bot:lookAt(owner)
bot:chat("Hi " .. owner.name .. "! Tell me: come, follow, stay or bye.")

local queue = spell:collect("BotMessageEvent")
while bot.online do
  local evt = queue:next() --[[@as BotMessageEvent]]
  if evt.bot == bot and evt.kind == "chat" and evt.sender == owner then
    local order = evt.message:lower()
    if order == "come" then
      bot:startPathTo(owner.pos)
    elseif order == "follow" then
      bot:startFollow(owner, { distance = 3 })
    elseif order == "stay" then
      bot:startStay()
    elseif order == "bye" then
      bot:chat("Bye!")
      bot:leave()
    end
  end
end

A Sparring Partner

For the game, a bot is a player like any other: it has a game mode and an inventory, and it fights with a player's attack cooldown. This one takes a wooden sword and attacks you until one of you falls. Play in survival mode, since a bot doesn't attack a player in creative mode:

local owner = spell.owner --[[@as Player]]
local hit = owner:raycastBlock(20)
local pos = hit and hit.blockPos + Vec3(0.5, 1, 0.5) or owner.eyePos + owner.lookVec * 5
local bot = Bot:spawn("MickKay", pos)
bot.gamemode = "survival"
bot.mainHandItem = Item("wooden_sword")
bot:chat("En garde!")
bot:startAttack(owner)
bot:awaitTask()
if bot.alive then
  bot:chat("Good fight!")
else
  bot:respawn()
  bot:chat("You win this time.")
end
sleep(40)
bot:leave()

What's next?

  • Explore complete examples:
  • The Lumberjack Spell that makes the whole tree fall when any player breaks a single log
  • The Guards who hold their posts against hostile mobs, and take them up again whenever their chunk is loaded
  • The Squire, a bot player who follows you, fights at your side behind its shield, and holds a spot when you tell it to
  • The Miner who digs a tunnel, lights it with torches, and fetches new tools from a chest when its pickaxe breaks
  • The Magic Hoe is a magical chain-harvesting tool
  • The Journey mini-game
  • The Discord Bridge connects the chat of your server with a Discord channel, and shows how to create the webhook and the Discord bot
  • Read the Commands documentation.