-------------- -- 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_add, vec_dist, vec_sub = vector.add, vector.distance, vector.subtract -- 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_primary.png^[multiply:#" .. col1 .. ")" local spots = "(creatura_spawning_crystal_secondary.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_primary.png^[multiply:#" .. def.col1 .. ")" local spots = "(creatura_spawning_crystal_secondary.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_time = def.min_time or 0, max_time = def.max_time or 24000, 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 tod = (minetest.get_timeofday() or 0) * 24000 local min_time = spawn.min_time local max_time = spawn.max_time local bounds_in = tod >= min_time and tod <= max_time local bounds_ex = tod >= max_time or tod <= min_time if (max_time > min_time and not bounds_in) or (min_time > max_time and not bounds_ex) then return 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) if #creatura.registered_mob_spawns < 1 then return end 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 = min_y, max_y 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) if not obj:get_yaw() then break end 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 protected_spawn = minetest.settings:get_bool("creatura_protected_spawn", true) 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 local min_abm_dist = tonumber(minetest.settings:get("creatura_min_abm_dist")) or 32 local function can_spawn(pos, width, height) local pos2 local w_iter = width / math.ceil(width) for y = 0, height, height / math.ceil(height) do for z = -width, width, w_iter do for x = -width, width, w_iter do pos2 = {x = pos.x + x, y = pos.y + y, z = pos.z + z} local def = creatura.get_node_def(pos2) if def.walkable then return false end end end end return true end 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_time = def.min_time or 0 local max_time = def.max_time or 24000 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 block_protected = def.block_protected_spawn or false local biomes = def.biomes or {} 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 not spawn_in_nodes then pos.y = pos.y + 1 end if (not protected_spawn or block_protected) and minetest.is_protected(pos, "") then return end if spawn_on_load then -- Manual checks for LBMs if random(chance) > 1 then return end if not minetest.find_node_near(pos, 1, neighbors) then return end if pos.y > max_height or pos.y < min_height then return end end local tod = (minetest.get_timeofday() or 0) * 24000 local bounds_in = tod >= min_time and tod <= max_time local bounds_ex = tod >= max_time or tod <= min_time if (max_time > min_time and not bounds_in) or (min_time > max_time and not bounds_ex) then return 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 if biomes and #biomes > 0 then local biome_id = minetest.get_biome_data(pos).biome local biome_name = minetest.get_biome_name(biome_id) local is_spawn_biome = false for _, biome in ipairs(biomes) do if biome:match("^" .. biome_name) then is_spawn_biome = true break end end if not is_spawn_biome then return end 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:get_luaentity() if ent and ent.name == mob then mob_count = mob_count + 1 if mob_count > spawn_cap then return end end if object:is_player() then plyr_found = true if vec_dist(pos, object:get_pos()) < min_abm_dist 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 = mob_def.collisionbox[5] if not can_spawn(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 offset = math.ceil(mob_width) local spawn_pos = creatura.get_ground_level({ x = pos.x + random(-offset, offset), y = pos.y, z = pos.x + random(-offset, offset), }, 3) if not can_spawn(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) if not obj:get_yaw() then break end 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) if not obj:get_yaw() then break end 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, min_y = min_height, max_y = max_height, catch_up = false, action = function(pos, ...) spawn_func(pos, ...) end }) end end