AI and saved data
Lesson 12.10
Your mobs can move on their own now: plan a path, walk it, open doors, climb ladders, and run goals that decide what to do next. And everything your mod knows can be saved: on a mob, on a player, on a chunk, or for the whole world.
Walk somewhere
local ok = mob:path_to(cl.vec(x, y, z), 1.0) -- 1.0 = walking speed; false: no path at all
mob:path_to(home, 1.2, { can_open_doors = true, can_climb = true })
if mob:path_done() then cl.log("arrived (or gave up)") end
mob:path_stop()
mob:path_state() tells you more: "none", "moving", "done" or "stuck".
To plan without moving, ask the level:
local path = world:find_path(mob.pos, target, { entity = mob })
if path and path:info().reaches_target then
cl.log("%d steps", #path:nodes())
end
Keep mobs away from places with avoid: return false for positions they must not use, or a number to make them less attractive.
mob:path_to(target, 1.0, { avoid = function(pos)
if world:light(pos).block >= 12 then return false end -- stay out of bright light
end })
Goals
A goal is a small plan with can_use (should I start?), start, tick and stop. CLight puts it into the mob’s own goal list, next to the vanilla goals:
cl.ai.add_goal(mob, 1, {
flags = { "look" },
state = { player = nil },
can_use = function(e, goal, s)
s.player = e.world:nearest_player(e.pos, 8)
return s.player ~= nil
end,
tick = function(e, goal, s) e:look_at(s.player) end,
})
- A lower priority number wins; two goals with the same flag (
"move","look","jump","target") never run together. stateis your goal’s own table: every callback gets it as the third argument.- Goals are not saved. Give every mob of a type a goal with
cl.ai.type_goal("minecraft:pig", 4, { ... })inlifecycle.common_setup: it is added again whenever a pig loads. mob:set_target(player)picks who a hostile mob attacks;mob:clear_goals()removes all goals (vanilla ones too) when you want full control.
Save data
Any Lua value goes: tables, numbers, strings, booleans.
player:set_data("quests", { active = "find_the_cave", done = {} })
local q = player:data("quests") -- nil when there is nothing
mob:set_data("owner", player.name) -- saved with the mob
world:chunk_at(player.pos):set_data("claim", { owner = player.name })
local stats = world:saved_data("stats") -- a table saved with the world
stats.visits = (stats.visits or 0) + 1 -- written at every save, no call needed
player:set_data(key, value, { sync = true }) also sends the value to the player’s Celestial client, where player:data(key) reads it (for a HUD, for example). Keys without a namespace get your mod’s id, so "quests" is "my_mod:quests".
Data with a default
cl.attachment gives a key a default value, and decides whether a player keeps it when they die (API 1.20):
cl.attachment("mana", { default = { value = 20 }, sync = true }) -- full again after a death
cl.attachment("souls", { default = 0, copy_on_death = true }) -- kept
cl.on("player.join", function(e)
print(e.player:data("mana").value) --> 20, even the first time
end)
More in the guide Data Attachments.
Exercise
Make /home save the player’s position on the player (/home set), and make a tamed mob follow the player home: /home go walks every mob tagged pet within 16 blocks to the saved spot.
Show a solution
cl.on("server.command_register", function()
cl.cmd.register(cl.cmd.literal("home")
:then_(cl.cmd.literal("set"):executes(function(ctx)
local p = ctx:player()
if not p then return 0 end
p:set_data("home", { x = p.pos.x, y = p.pos.y, z = p.pos.z })
p:send_message("Home saved.")
return 1
end))
:then_(cl.cmd.literal("go"):executes(function(ctx)
local p = ctx:player()
local home = p and p:data("home")
if not home then return 0 end
for _, pet in ipairs(p.world:entities_near(p.pos, 16, { tag = "pet" })) do
pet:path_to(cl.vec(home.x, home.y, home.z), 1.2, { can_open_doors = true })
end
return 1
end)))
end)
Recap: path_to and find_path move and plan, cl.ai.add_goal and cl.ai.type_goal add goals, data / set_data keep values on entities and chunks, and saved_data keeps them for the world. More in the guides AI and Pathfinding and Saved Data.