creatura/spawning.lua
2022-08-27 20:07:53 -07:00

622 lines
No EOL
18 KiB
Lua

--------------
-- Spawning --
--------------
creatura.registered_mob_spawns = {}
local walkable_nodes = {}
minetest.register_on_mods_loaded(function()
for name in pairs(minetest.registered_nodes) do
if name ~= "air" and name ~= "ignore" then
if minetest.registered_nodes[name].walkable then
table.insert(walkable_nodes, name)
end
end
end
end)
-- Math --
local abs = math.abs
local pi = math.pi
local random = math.random
local function vec_raise(v, n)
return {x = v.x, y = v.y + n, z = v.z}
end
local vec_sub = vector.subtract
local vec_add = vector.add
-- Registration --
local creative = minetest.settings:get_bool("creative_mode")
local function format_name(str)
if str then
if str:match(":") then str = str:split(":")[2] end
return (string.gsub(" " .. str, "%W%l", string.upper):sub(2):gsub("_", " "))
end
end
function creatura.register_spawn_egg(name, col1, col2, inventory_image) -- deprecated
if col1 and col2 then
local base = "(creatura_spawning_crystal.png^[multiply:#" .. col1 .. ")"
local spots = "(creatura_spawning_crystal_overlay.png^[multiply:#" .. col2 .. ")"
inventory_image = base .. "^" .. spots
end
local mod_name = name:split(":")[1]
local mob_name = name:split(":")[2]
minetest.register_craftitem(mod_name .. ":spawn_" .. mob_name, {
description = "Spawn " .. format_name(name),
inventory_image = inventory_image,
stack_max = 99,
on_place = function(itemstack, _, pointed_thing)
local mobdef = minetest.registered_entities[name]
local spawn_offset = abs(mobdef.collisionbox[2])
local pos = minetest.get_pointed_thing_position(pointed_thing, true)
pos.y = (pos.y - 0.4) + spawn_offset
local object = minetest.add_entity(pos, name)
if object then
object:set_yaw(random(1, 6))
object:get_luaentity().last_yaw = object:get_yaw()
end
if not creative then
itemstack:take_item()
return itemstack
end
end
})
end
function creatura.register_spawn_item(name, def)
local inventory_image
if not def.inventory_image
and def.col1 and def.col2 then
local base = "(creatura_spawning_crystal.png^[multiply:#" .. def.col1 .. ")"
local spots = "(creatura_spawning_crystal_overlay.png^[multiply:#" .. def.col2 .. ")"
inventory_image = base .. "^" .. spots
end
local mod_name = name:split(":")[1]
local mob_name = name:split(":")[2]
def.description = def.description or "Spawn " .. format_name(name)
def.inventory_image = def.inventory_image or inventory_image
def.on_place = function(itemstack, player, pointed_thing)
local pos = minetest.get_pointed_thing_position(pointed_thing, true)
if minetest.is_protected(pos, player and player:get_player_name() or "") then return end
local mobdef = minetest.registered_entities[name]
local spawn_offset = abs(mobdef.collisionbox[2])
pos.y = (pos.y - 0.49) + spawn_offset
if def.antispam then
local objs = minetest.get_objects_in_area(vec_sub(pos, 0.51), vec_add(pos, 0.51))
for _, obj in ipairs(objs) do
if obj
and obj:get_luaentity()
and obj:get_luaentity().name == name then
return
end
end
end
local object = minetest.add_entity(pos, name)
if object then
object:set_yaw(random(0, pi * 2))
object:get_luaentity().last_yaw = object:get_yaw()
end
if not minetest.is_creative_enabled(player:get_player_name())
or def.consume_in_creative then
itemstack:take_item()
return itemstack
end
end
minetest.register_craftitem(mod_name .. ":spawn_" .. mob_name, def)
end
function creatura.register_mob_spawn(name, def)
local spawn = {
chance = def.chance or 5,
min_height = def.min_height or 0,
max_height = def.max_height or 128,
min_light = def.min_light or 6,
max_light = def.max_light or 15,
min_group = def.min_group or 1,
max_group = def.max_group or 4,
nodes = def.nodes or nil,
biomes = def.biomes or nil,
spawn_cluster = def.spawn_cluster or false,
spawn_on_gen = def.spawn_on_gen or false,
spawn_in_nodes = def.spawn_in_nodes or false,
spawn_cap = def.spawn_cap or 5,
send_debug = def.send_debug or false
}
creatura.registered_mob_spawns[name] = spawn
end
creatura.registered_on_spawns = {}
function creatura.register_on_spawn(name, func)
if not creatura.registered_on_spawns[name] then
creatura.registered_on_spawns[name] = {}
end
table.insert(creatura.registered_on_spawns[name], func)
end
-- Utility Functions --
local function table_contains(tbl, val)
for _, v in pairs(tbl) do
if v == val then
return true
end
end
return false
end
local function get_spawnable_mobs(pos, mapgen)
local biome = minetest.get_biome_name(minetest.get_biome_data(pos).biome)
if not biome then return end
local mobs = {}
for mob, data in pairs(creatura.registered_mob_spawns) do
local biome_valid = data.biomes and table_contains(data.biomes, biome)
if (biome_valid
or data.nodes)
and (not mapgen
or data.spawn_on_gen)
and not data.spawn_in_nodes then
table.insert(mobs, mob)
end
end
return mobs
end
local function is_spawn_node(name, spawn_nodes)
for _, val in ipairs(spawn_nodes) do
if val:match("^group:") then
local group = val:split(":")[2]
if minetest.get_item_group(name, group) > 0 then
return true
end
elseif val == name then
return true
end
end
return false
end
-- Spawning Function --
local min_spawn_radius = 32
local max_spawn_radius = 128
local function execute_spawns(player)
if not player:get_pos() then return end
local pos = player:get_pos()
local spawn_pos_center = {
x = pos.x + random(-max_spawn_radius, max_spawn_radius),
y = pos.y,
z = pos.z + random(-max_spawn_radius, max_spawn_radius)
}
local spawnable_mobs = get_spawnable_mobs(spawn_pos_center)
if spawnable_mobs
and #spawnable_mobs > 0 then
local mob = spawnable_mobs[random(#spawnable_mobs)]
local spawn = creatura.registered_mob_spawns[mob]
if not spawn
or random(spawn.chance or 2) > 1 then return end
-- Spawn cap check
local objects = minetest.get_objects_inside_radius(pos, max_spawn_radius)
local object_count = 0
for _, object in ipairs(objects) do
if creatura.is_alive(object)
and not object:is_player()
and object:get_luaentity().name == mob then
object_count = object_count + 1
end
end
if object_count >= spawn.spawn_cap then
return
end
local index_func
if spawn.spawn_in_nodes then
index_func = minetest.find_nodes_in_area
else
index_func = minetest.find_nodes_in_area_under_air
end
local spawn_on = spawn.nodes or walkable_nodes
if type(spawn_on) == "string" then
spawn_on = {spawn_on}
end
local spawn_y_array = index_func(
vec_raise(spawn_pos_center, -max_spawn_radius),
vec_raise(spawn_pos_center, max_spawn_radius),
spawn_on)
if spawn_y_array[1] then
local spawn_pos = spawn_y_array[1]
local dist = vector.distance(pos, spawn_pos)
if dist < min_spawn_radius or dist > max_spawn_radius then
return
end
if spawn_pos.y > spawn.max_height
or spawn_pos.y < spawn.min_height then
return
end
if not spawn.spawn_in_nodes then
spawn_pos = vec_raise(spawn_pos, 1)
end
local light = minetest.get_node_light(spawn_pos) or 7
if light > spawn.max_light
or light < spawn.min_light then
return
end
local mob_def = minetest.registered_entities[mob]
local mob_width = mob_def.collisionbox[4]
local mob_height = math.max(0, mob_def.collisionbox[5] - mob_def.collisionbox[2])
if not creatura.is_pos_moveable(spawn_pos, mob_width, mob_height) then
return
end
local group_size = random(spawn.min_group or 1, spawn.max_group or 1)
if spawn.spawn_cluster then
minetest.add_node(spawn_pos, {name = "creatura:spawn_node"})
local meta = minetest.get_meta(spawn_pos)
meta:set_string("mob", mob)
meta:set_int("cluster", group_size)
else
for _ = 1, group_size do
spawn_pos = {
x = spawn_pos.x + random(-3, 3),
y = spawn_pos.y,
z = spawn_pos.z + random(-3, 3)
}
spawn_pos = creatura.get_ground_level(spawn_pos, 4)
minetest.add_node(spawn_pos, {name = "creatura:spawn_node"})
local meta = minetest.get_meta(spawn_pos)
meta:set_string("mob", mob)
end
end
if spawn.send_debug then
minetest.chat_send_all(mob .. " spawned at " .. minetest.pos_to_string(spawn_pos))
end
end
end
end
local spawn_step = tonumber(minetest.settings:get("creatura_spawn_step")) or 15
local spawn_tick = 0
minetest.register_globalstep(function(dtime)
spawn_tick = spawn_tick - dtime
if spawn_tick <= 0 then
for _, player in ipairs(minetest.get_connected_players()) do
execute_spawns(player)
end
spawn_tick = spawn_step
end
end)
-- Mapgen Spawning --
local mapgen_spawning = minetest.settings:get_bool("creatura_mapgen_spawning", true)
local mapgen_spawning_int = tonumber(minetest.settings:get("creatura_mapgen_spawn_interval")) or 5
local spawn_cooldown = 1
minetest.register_on_generated(function(minp, maxp)
if not mapgen_spawning then return end
spawn_cooldown = spawn_cooldown - 1
if spawn_cooldown <= 0 then
local min_x, max_x = minp.x, maxp.x
local min_y, max_y = minp.y, maxp.y
local min_z, max_z = minp.z, maxp.z
local mobs = get_spawnable_mobs({
x = min_x + (max_x - min_x) * 0.5,
y = min_y + (max_y - min_y) * 0.5,
z = min_z + (max_z - min_z) * 0.5
}, true)
if not mobs then spawn_cooldown = mapgen_spawning_int return end
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
local data = vm:get_data()
for z = min_z + 8, max_z - 7, 8 do
for x = min_x + 8, max_x - 7, 8 do
local mob = #mobs > 0 and mobs[random(#mobs)]
if mob then
local spawn = creatura.registered_mob_spawns[mob]
for y = max_y, min_y, -1 do
if y > spawn.max_height
or y < spawn.min_height then
break
end
local vi = area:index(x, y, z)
local vi_name = minetest.get_name_from_content_id(data[vi])
if spawn.spawn_in_nodes
and is_spawn_node(vi_name, spawn.nodes) then
-- Add Spawn Node to Map
data[vi] = minetest.get_content_id("creatura:spawn_node")
vm:set_data(data)
vm:write_to_map()
-- Apply Meta
local group_size = random(spawn.min_group or 1, spawn.max_group or 1)
local meta = minetest.get_meta({x = x, y = y, z = z})
meta:set_string("mob", mob)
meta:set_int("cluster", group_size)
spawn_cooldown = mapgen_spawning_int
return
end
local vi_n = area:index(x, y - 1, z)
local vi_n_name = minetest.get_name_from_content_id(data[vi_n])
if not creatura.get_node_def(vi_name).walkable
and ((spawn.nodes and is_spawn_node(vi_n_name, spawn.nodes))
or (not spawn.nodes and creatura.get_node_def(vi_n_name).walkable)) then
-- Add Spawn Node to Map
data[vi] = minetest.get_content_id("creatura:spawn_node")
vm:set_data(data)
vm:write_to_map()
-- Apply Meta
local group_size = random(spawn.min_group or 1, spawn.max_group or 1)
local meta = minetest.get_meta({x = x, y = y, z = z})
meta:set_string("mob", mob)
meta:set_int("cluster", group_size)
spawn_cooldown = mapgen_spawning_int
return
end
end
end
end
end
end
end)
-- Node --
minetest.register_node("creatura:spawn_node", {
drawtype = "airlike",
groups = {not_in_creative_inventory = 1},
walkable = false
})
local spawn_interval = tonumber(minetest.settings:get("creatura_spawn_interval")) or 10
minetest.register_abm({
label = "Creatura Spawning",
nodenames = {"creatura:spawn_node"},
interval = spawn_interval,
chance = 1,
action = function(pos)
local meta = minetest.get_meta(pos)
local name = meta:get_string("mob") or ""
if name == "" then minetest.remove_node(pos) return end
local amount = meta:get_int("cluster")
local obj
if amount > 0 then
for _ = 1, amount do
obj = minetest.add_entity(pos, name)
end
minetest.log("action",
"[Creatura] Spawned " .. amount .. " " .. name .. " at " .. minetest.pos_to_string(pos))
else
obj = minetest.add_entity(pos, name)
minetest.log("action",
"[Creatura] Spawned a " .. name .. " at " .. minetest.pos_to_string(pos))
end
minetest.remove_node(pos)
if obj
and creatura.registered_on_spawns[name]
and #creatura.registered_on_spawns[name] > 0 then
for i = 1, #creatura.registered_on_spawns[name] do
local func = creatura.registered_on_spawns[name][i]
func(obj:get_luaentity(), pos)
end
end
end,
})
--[[minetest.register_lbm({
name = "creatura:spawning",
nodenames = {"creatura:spawn_node"},
run_at_every_load = true,
action = function(pos)
local meta = minetest.get_meta(pos)
local name = meta:get_string("mob")
local amount = meta:get_int("cluster")
local obj
if amount > 0 then
for i = 1, amount do
obj = minetest.add_entity(pos, name)
end
else
obj = minetest.add_entity(pos, name)
end
minetest.remove_node(pos)
if obj
and creatura.registered_on_spawns[name]
and #creatura.registered_on_spawns[name] > 0 then
for i = 1, #creatura.registered_on_spawns[name] do
local func = creatura.registered_on_spawns[name][i]
func(obj:get_luaentity(), pos)
end
end
end,
})]]
------------------
-- ABM Spawning --
------------------
--[[creatura.register_abm_spawn("mymod:mymob", {
chance = 3000,
interval = 30,
min_height = 0,
max_height = 128,
min_light = 1,
max_light = 15,
min_group = 1,
max_group = 4,
nodes = {"group:soil", "group:stone"},
neighbors = {"air"},
spawn_on_load = false,
spawn_in_nodes = false,
spawn_cap = 5
})]]
local abr = (tonumber(minetest.get_mapgen_setting("active_block_range")) or 4) * 16
local max_per_block = tonumber(minetest.settings:get("creatura_mapblock_limit")) or 99
function creatura.register_abm_spawn(mob, def)
local chance = def.chance or 3000
local interval = def.interval or 30
local min_height = def.min_height or 0
local max_height = def.max_height or 128
local min_light = def.min_light or 1
local max_light = def.max_light or 15
local min_group = def.min_group or 1
local max_group = def.max_group or 4
local nodes = def.nodes or {"group:soil", "group:stone"}
local neighbors = def.neighbors or {"air"}
local spawn_on_load = def.spawn_on_load or false
local spawn_in_nodes = def.spawn_in_nodes or false
local spawn_cap = def.spawn_cap or 5
local function spawn_func(pos, _, _, aocw)
if spawn_on_load
and random(chance) > 1 then
return
end
if spawn_on_load
and not minetest.find_node_near(pos, 1, neighbors) then
return
end
if pos.y > max_height
or pos.y < min_height then
return
end
if not spawn_in_nodes then
pos = vec_raise(pos, 1)
end
local light = minetest.get_node_light(pos) or 7
if light > max_light
or light < min_light then
return
end
if aocw
and aocw >= max_per_block then
return
end
local mob_count = 0
local plyr_found = false
local objects = minetest.get_objects_inside_radius(pos, abr)
for _, object in ipairs(objects) do
local ent = object and object:get_luaentity()
local is_plyr = object and object:is_player()
plyr_found = plyr_found or is_plyr
if ent
and ent.name == mob then
mob_count = mob_count + 1
if mob_count > spawn_cap then
return
end
end
end
if not plyr_found then
return
end
local mob_def = minetest.registered_entities[mob]
local mob_width = mob_def.collisionbox[4]
local mob_height = math.max(0, mob_def.collisionbox[5] - mob_def.collisionbox[2])
pos.y = pos.y - 0.49
if not creatura.is_pos_moveable(pos, mob_width, mob_height) then
return
end
local group_size = random(min_group or 1, max_group or 1)
if group_size > 1 then
for _ = 1, group_size do
local spawn_pos = creatura.get_ground_level({
x = pos.x + random(-mob_width, mob_width),
y = pos.y,
z = pos.x + random(-mob_width, mob_width),
}, 3)
if not creatura.is_pos_moveable(spawn_pos, mob_width, mob_height) then
spawn_pos = pos
end
local obj = minetest.add_entity(spawn_pos, mob)
if obj
and creatura.registered_on_spawns[mob]
and #creatura.registered_on_spawns[mob] > 0 then
for i = 1, #creatura.registered_on_spawns[mob] do
local func = creatura.registered_on_spawns[mob][i]
func(obj:get_luaentity(), pos)
end
end
end
else
local obj = minetest.add_entity(pos, mob)
if obj
and creatura.registered_on_spawns[mob]
and #creatura.registered_on_spawns[mob] > 0 then
for i = 1, #creatura.registered_on_spawns[mob] do
local func = creatura.registered_on_spawns[mob][i]
func(obj:get_luaentity(), pos)
end
end
end
minetest.log("action",
"[Creatura] [ABM Spawning] Spawned " .. group_size .. " " .. mob .. " at " .. minetest.pos_to_string(pos))
end
if spawn_on_load then
minetest.register_lbm({
name = mob .. "_spawning",
label = mob .. " spawning",
nodenames = nodes,
run_at_every_load = false,
action = function(pos)
spawn_func(pos)
end
})
else
minetest.register_abm({
label = mob .. " spawning",
nodenames = nodes,
neighbors = neighbors,
interval = interval,
chance = chance,
catch_up = false,
action = function(pos, ...)
spawn_func(pos, ...)
end
})
end
end