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
Print a Block's ID, Name, and Data¶
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:
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:
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.