2. The arena region
The arena needs borders: to protect its blocks now, and later to notice who walks in or out. A region is a box in a world that knows which entities are inside it.
Two corners
Section titled “Two corners”The admin stands in one corner and runs /arena pos1, then in the opposite corner /arena pos2. Both
commands do the same thing with a different key, so they share a function:
local function corner(ctx, key) local pos = ctx.player.pos config[key] = { x = math.floor(pos.x), y = math.floor(pos.y), z = math.floor(pos.z), world = ctx.player.world.name, } buildRegion() local ready = region and ", the arena region is ready." or "." ctx.player:sendMessage("Corner " .. key .. " set" .. ready)end
register("arena pos1", function(ctx) corner(ctx, "pos1") end, ADMIN)register("arena pos2", function(ctx) corner(ctx, "pos2") end, ADMIN)math.floor turns the player’s position (say 12.7, 64, -3.2) into the block they stand in
(12, 64, -4).
Building the region
Section titled “Building the region”Regions, like everything else a script creates, are removed on /ignis reload. So the region is not
saved; the corners are, and the script builds the region from them every time it loads:
local region -- Region|nil, created by buildRegion() belowlocal function buildRegion() if region then region:destroy() end region = nil local a, b = config.pos1, config.pos2 if not (a and b) then return end
-- A region is a box from its min corner up to (but not including) its max -- corner, so +1 makes the max block part of it. region = mc.world(a.world):createRegion( vec(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)), vec(math.max(a.x, b.x) + 1, math.max(a.y, b.y) + 1, math.max(a.z, b.z) + 1) )end
buildRegion()The corners can be given in any order, hence min and max. The + 1 matters: the region box ends
before its second corner, and a block at x = 20 spans from 20 to 21.
region is declared near the top of the file because functions above buildRegion need it. The arena is
ready once it has a region, a lobby and a spawn point, and join now checks exactly that:
local function isReady() return region ~= nil and config.lobby ~= nil and #config.spawns > 0end if not isReady() then player:sendMessage("§cThe arena is not set up yet.")Protecting the blocks
Section titled “Protecting the blocks”block.break and block.place are cancellable events: a handler that calls e:cancel() stops the
action. e.pos is the block and e.player the player:
local function isProtected(player, pos) return region ~= nil and region:contains(pos) and not player:hasPermission(ADMIN)end
mc.on("block.break", function(e) if isProtected(e.player, e.pos) then e:cancel() endend, { priority = "high" })
mc.on("block.place", function(e) if isProtected(e.player, e.pos) then e:cancel() endend, { priority = "high" })Admins keep building rights, so they can still fix the arena.
priority = "high" makes these handlers run before handlers with the default priority. Once an event is
cancelled, later handlers are not called at all, so a script elsewhere that pays players for mined blocks
never pays for a block the arena refused. See Events for the details.
Try it
Section titled “Try it”- Stand in one corner of your arena (on the floor), run
/arena pos1; in the opposite corner, one block above the highest point players should reach, run/arena pos2. - Ask a player who is not an operator to break a block inside the arena and one outside it. (Alone? Run
/deop <you>from the server console and/op <you>afterwards.) /ignis eval #me.world.regionsshows how many regions the world has. It stays at 1 however often you reload.
Checkpoint
Section titled “Checkpoint”config/ignis/arena.lua after chapter 2
-- config/ignis/arena.lua: a last-player-standing PvP arena.
local ADMIN = "arena.admin"
-- Corners, spawn points and lobby, kept across restarts by mc.data.mc.data.arena = mc.data.arena or {}local config = mc.data.arenaconfig.spawns = config.spawns or {}
-- Where a player stands, as a plain table that can be saved.local function here(player) local p = player.pos return { x = p.x, y = p.y, z = p.z, world = player.world.name }end
local function teleport(player, point) player:teleport(point.x, point.y, point.z, point.world)end
local region -- Region|nil, created by buildRegion() below
local function isReady() return region ~= nil and config.lobby ~= nil and #config.spawns > 0end
local function count(t) local n = 0 for _ in pairs(t) do n = n + 1 end return nend
-- Game state. It lives only in memory: a reload starts from scratch.local game = { players = {}, -- uuid -> Player: everyone who joined}
local function tell(text) for _, player in pairs(game.players) do player:sendMessage(text) endend
-- ── Joining and leaving ────────────────────────────────────────────────────
local function join(player) if not isReady() then player:sendMessage("§cThe arena is not set up yet.") elseif game.players[player.uuid] then player:sendMessage("§eYou have already joined.") else game.players[player.uuid] = player teleport(player, config.lobby) tell("§a" .. player.name .. " §7joined the arena (" .. count(game.players) .. ")") endend
local function leave(player) local uuid = player.uuid if not game.players[uuid] then return end game.players[uuid] = nil player:sendMessage("§7You left the arena.")end
mc.on("player.leave", function(e) leave(e.player)end)
-- ── The arena region ───────────────────────────────────────────────────────
local function isProtected(player, pos) return region ~= nil and region:contains(pos) and not player:hasPermission(ADMIN)end
mc.on("block.break", function(e) if isProtected(e.player, e.pos) then e:cancel() endend, { priority = "high" })
mc.on("block.place", function(e) if isProtected(e.player, e.pos) then e:cancel() endend, { priority = "high" })
local function buildRegion() if region then region:destroy() end region = nil local a, b = config.pos1, config.pos2 if not (a and b) then return end
-- A region is a box from its min corner up to (but not including) its max -- corner, so +1 makes the max block part of it. region = mc.world(a.world):createRegion( vec(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)), vec(math.max(a.x, b.x) + 1, math.max(a.y, b.y) + 1, math.max(a.z, b.z) + 1) )end
buildRegion()
-- ── Commands ───────────────────────────────────────────────────────────────
local function corner(ctx, key) local pos = ctx.player.pos config[key] = { x = math.floor(pos.x), y = math.floor(pos.y), z = math.floor(pos.z), world = ctx.player.world.name, } buildRegion() local ready = region and ", the arena region is ready." or "." ctx.player:sendMessage("Corner " .. key .. " set" .. ready)end
register("arena pos1", function(ctx) corner(ctx, "pos1") end, ADMIN)register("arena pos2", function(ctx) corner(ctx, "pos2") end, ADMIN)
register("arena addspawn", function(ctx) local spawns = config.spawns spawns[#spawns + 1] = here(ctx.player) ctx.player:sendMessage("Spawn point #" .. #spawns .. " added.")end, ADMIN)
register("arena clearspawns", function(ctx) config.spawns = {} ctx.player:sendMessage("All spawn points removed.")end, ADMIN)
register("arena setlobby", function(ctx) config.lobby = here(ctx.player) ctx.player:sendMessage("Lobby set.")end, ADMIN)
register("arena join", function(ctx) join(ctx.player) end)register("arena leave", function(ctx) leave(ctx.player) end)Exercise: show the arena borders: after /arena pos2, spawn minecraft:end_rod particles at the four
bottom corners with world:particle.