From 725eafb6e0e5739ffb0b837290b319949e096e37 Mon Sep 17 00:00:00 2001 From: ElCeejo Date: Mon, 17 Jul 2023 18:04:30 -0700 Subject: [PATCH] Fixed mapgen spawning, Deprecate globalstep spawning --- api.lua | 2 +- doc.txt | 45 ++- pathfinding.lua | 15 +- settingtypes.txt | 5 +- spawning.lua | 792 +++++++++++++++++++---------------------------- 5 files changed, 380 insertions(+), 479 deletions(-) diff --git a/api.lua b/api.lua index 1d9dafb..e766322 100644 --- a/api.lua +++ b/api.lua @@ -516,7 +516,7 @@ function creatura.basic_punch_func(self, puncher, tflp, tool_caps, dir) if puncher:is_player() then tool = puncher:get_wielded_item() tool_name = tool:get_name() - add_wear = not minetest.is_creative_enabled(puncher) + add_wear = not minetest.is_creative_enabled(puncher:get_player_name()) end if (self.immune_to and contains_val(self.immune_to, tool_name)) then diff --git a/doc.txt b/doc.txt index 494d9c6..b98e800 100644 --- a/doc.txt +++ b/doc.txt @@ -207,6 +207,13 @@ Environment access Global Mob API -------------- +* `creatura.default_water_physics(self)` + * Bouyancy and Drag physics used by default on all Mobs + +* `creatura.default_vitals(self)` + * Vitals used by default on all Mobs + * Handles suffocation, drowning, fire damage, and fall damage + * `creatura.drop_items(self)` * Drops items from `self.drops` @@ -236,4 +243,40 @@ The maximum amount of time the pathfinder can spend per-step (in microseconds) c * Finds a path from `pos1` to `pos2` * Returns a path with arbitrary angles for natural looking paths at the expense of performance * `get_neighbors` is a function used to find valid neighbors - * `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default \ No newline at end of file + * `creatura.pathfinder.get_neighbors_fly` and `creatura.pathfinder.get_neighbors_swim` are bundled by default + +Spawning +-------- + +NOTE: Globalstep spawning from early versions of the API likely won't recieve much/any support going forward. Use ABM Spawning instead. + +* `creatura.register_abm_spawn(name, def)` + * `name` of the mob to spawn + * `def` is a table of spawn parameters + * `chance` is the chance of a mob spawning every `interval` + * (a `chance` of 30 and `interval` of 60 would mean a 1 in 30 chance of a mob spawning every 60 seconds) + * `chance_on_load` same as `chance` but for LBM spawning (when a chunk is loaded for the first time) + * `interval` is how often (in seconds) a spawn attempt will happen + * `min_height` is the minimum height that a spawn attempt can happen at + * a `min_height` of 0 would mean the mob cannot spawn below a y coordinate of 0 + * `max_height` is the maximum height that a spawn attempt can happen at + * a `max_height` of 128 would mean the mob cannot spawn above a y coordinate of 128 + * `min_time` is the minimum time a mob can spawn at + * `max_time` is the maximum time a mob can spawn at + * set `min_time` to 19500 and `max_time` to 4500 to only spawn at night and swap the numbers to only spawn at day + * `min_light` is the minimum light level a mob can spawn at + * `max_light` is the maximum light level a mob can spawn at + * `min_group` is the lowest number of mobs to spawn in a group at a time + * value of 3 means the mob will always spawn with at least 3 mobs together + * `max_group` is the highest number of mobs to spawn in a group at a time + * `block_protected` will block spawning mobs in protected areas if set to true + * `biomes` is a table of biomes the mob can spawn in + * `nodes` is a table of nodes the mob can spawn in/on + * `neighbors` is a table of nodes that must be adjacent to the spawn position + * ex: set to `{"groups:tree"}` to force the mob to spawn next to a tree + * `spawn_on_load` will spawn mobs when a chunk generates if set to true + * `spawn_active` will spawn mobs at the rate set by `interval` if set to true + * this option is true by default. setting it to false without setting `spawn_on_load` to true will disable spawning the mob. + * `spawn_in_nodes` will spawn mobs inside the node rather than above if set to true + * set this to true for mobs that spawn in water + * `spawn_cap` is the maximum amount of the mob that can spawn within active block range \ No newline at end of file diff --git a/pathfinding.lua b/pathfinding.lua index 4dd6d41..83480e5 100644 --- a/pathfinding.lua +++ b/pathfinding.lua @@ -133,7 +133,7 @@ local neighbor_grid_3d = { -- Get Neighbors -local function get_neighbors(pos, width, height, open, closed, parent) +local function get_neighbors(pos, width, height, open, closed, parent, evaluated) local result = {} local neighbor local can_move @@ -151,7 +151,8 @@ local function get_neighbors(pos, width, height, open, closed, parent) end if open[hashed_pos] - or closed[hashed_pos] then + or closed[hashed_pos] + or evaluated[hashed_pos] then can_move = false elseif can_move then can_move = not is_blocked(neighbor, width, height) @@ -172,6 +173,8 @@ local function get_neighbors(pos, width, height, open, closed, parent) if can_move then table.insert(result, neighbor) end + + evaluated[hashed_pos] = true end return result end @@ -303,6 +306,7 @@ function creatura.pathfinder.find_path(self, pos1, pos2, neighbor_func) local openSet = self._path_data.open or {} local closedSet = self._path_data.closed or {} + local evaluated = {} local start_index = minetest.hash_node_position(start) @@ -382,7 +386,8 @@ function creatura.pathfinder.find_path(self, pos1, pos2, neighbor_func) self.height, openSet, closedSet, - (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos) + (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos), + evaluated ) -- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached @@ -463,6 +468,7 @@ function creatura.pathfinder.find_path_theta(self, pos1, pos2, neighbor_func) local openSet = self._path_data.open or {} local closedSet = self._path_data.closed or {} + local evaluated = {} local start_index = minetest.hash_node_position(start) @@ -543,7 +549,8 @@ function creatura.pathfinder.find_path_theta(self, pos1, pos2, neighbor_func) self.height, openSet, closedSet, - (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos) + (parent_closed and parent_closed.pos) or (parent_open and parent_open.pos), + evaluated ) -- Fly, Swim, and Climb all return true for check_vertical to properly check if goal has been reached diff --git a/settingtypes.txt b/settingtypes.txt index 6bb35e1..090be0d 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -7,14 +7,11 @@ creatura_step_type (Step Type) enum simple simple,fancy # How often (in seconds) the spawn ABM is called creatura_spawn_interval (Spawn ABM Interval) int 10 -# Time (in seconds) between spawn steps -creatura_spawn_step (Spawn Step Interval) int 15 - # Allows Mobs to spawn during chunk generation (If dependent mods use spawn_on_gen) creatura_mapgen_spawning (Mapgen Spawning) bool true # How many chunks are generated before a Mob can spawn -creatura_mapgen_spawn_interval (Mapgen Spawning Interval) int 5 +creatura_mapgen_spawn_interval (Mapgen Spawning Interval) int 64 # How many Mobs can be a in a Mapblock before ABM spawning is blocked creatura_mapblock_limit (Max Mobs per Mapblock) int 12 diff --git a/spawning.lua b/spawning.lua index 05c80f8..c7c9a0f 100644 --- a/spawning.lua +++ b/spawning.lua @@ -3,18 +3,7 @@ -------------- 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) +creatura.registered_on_spawns = {} -- Math -- @@ -22,17 +11,11 @@ local abs = math.abs local ceil = math.ceil local pi = math.pi local random = math.random -local min, max = math.min, math.max - -local function vec_raise(v, n) - return {x = v.x, y = v.y + n, z = v.z} -end +local min = math.min local vec_add, vec_dist, vec_sub = vector.add, vector.distance, vector.subtract --- Registration -- - -local creative = minetest.settings:get_bool("creative_mode") +-- Utility Functions -- local function format_name(str) if str then @@ -41,36 +24,56 @@ local function format_name(str) 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 +local function table_contains(tbl, val) + for _, v in pairs(tbl) do + if v == val then + return true + end 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 + return false +end + +local function pos_meets_params(pos, def) + if not minetest.find_nodes_in_area(pos, pos, def.nodes) then return false end + if not minetest.find_node_near(pos, 1, def.neighbors) then return false end + + return true +end + +local function can_spawn(pos, width, height) + local pos2 + local w_iter = width / ceil(width) + for y = 0, height, height / 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 +local function do_on_spawn(pos, obj) + local name = obj and obj:get_luaentity().name + if not name then return end + local spawn_functions = creatura.registered_on_spawns[name] or {} + + if #spawn_functions > 0 then + for _, func in ipairs(spawn_functions) do + func(obj:get_luaentity(), pos) + if not obj:get_yaw() then break end + end + end +end + +---------------- +-- Spawn Item -- +---------------- + +local creative = minetest.settings:get_bool("creative_mode") + function creatura.register_spawn_item(name, def) local inventory_image if not def.inventory_image @@ -132,30 +135,6 @@ function creatura.register_spawn_item(name, def) minetest.register_craftitem(def.itemstring or (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] = {} @@ -163,336 +142,9 @@ function creatura.register_on_spawn(name, func) 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 = vec_dist(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 = 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) - group_size = min(spawn.spawn_cap - object_count, group_size) - - 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 -- ------------------- +-------------- +-- Spawning -- +-------------- --[[creatura.register_abm_spawn("mymod:mymob", { chance = 3000, @@ -516,26 +168,12 @@ local max_per_block = tonumber(minetest.settings:get("creatura_mapblock_limit")) local max_in_abr = tonumber(minetest.settings:get("creatura_abr_limit")) or 24 local min_abm_dist = min(abr / 2, tonumber(minetest.settings:get("creatura_min_abm_dist")) or 32) -local function can_spawn(pos, width, height) - local pos2 - local w_iter = width / ceil(width) - for y = 0, height, height / 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 - local mobs_spawn = minetest.settings:get_bool("mobs_spawn") ~= false +local mapgen_mobs = {} + function creatura.register_abm_spawn(mob, def) local chance = def.chance or 3000 - local chance_on_load = def.chance_on_load or def.chance / 32 local interval = def.interval or 30 local min_height = def.min_height or 0 local max_height = def.max_height or 128 @@ -550,11 +188,10 @@ function creatura.register_abm_spawn(mob, def) 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_active = def.spawn_active or true local spawn_in_nodes = def.spawn_in_nodes or false local spawn_cap = def.spawn_cap or 5 - local function spawn_func(pos, aocw, is_lbm) + local function spawn_func(pos, aocw) if not mobs_spawn then return @@ -570,12 +207,6 @@ function creatura.register_abm_spawn(mob, def) return end - if is_lbm then -- Manual checks for LBMs - if random(chance_on_load or 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 @@ -648,11 +279,14 @@ function creatura.register_abm_spawn(mob, def) end local group_size = random(min_group or 1, max_group or 1) + local obj if group_size > 1 then + local offset + local spawn_pos for _ = 1, group_size do - local offset = ceil(mob_width) - local spawn_pos = creatura.get_ground_level({ + offset = ceil(mob_width) + spawn_pos = creatura.get_ground_level({ x = pos.x + random(-offset, offset), y = pos.y, z = pos.z + random(-offset, offset) @@ -660,28 +294,12 @@ function creatura.register_abm_spawn(mob, def) 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 + obj = minetest.add_entity(spawn_pos, mob) + do_on_spawn(spawn_pos, obj) 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 + obj = minetest.add_entity(pos, mob) + do_on_spawn(pos, obj) end minetest.log("action", @@ -689,30 +307,266 @@ function creatura.register_abm_spawn(mob, def) end + 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, _, _, aocw) + spawn_func(pos, aocw) + 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, _, _, aocw) - spawn_func(pos, aocw, true) - end - }) - end - if spawn_active then - 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, _, _, aocw) - spawn_func(pos, aocw, false) - end - }) + table.insert(mapgen_mobs, mob) end + + creatura.registered_mob_spawns[mob] = { + chance = def.chance or 3000, + interval = def.interval or 30, + 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 1, + max_light = def.max_light or 15, + min_group = def.min_group or 1, + max_group = def.max_group or 4, + block_protected = def.block_protected_spawn or false, + biomes = def.biomes or {}, + nodes = def.nodes or {"group:soil", "group:stone"}, + neighbors = def.neighbors or {"air"}, + spawn_on_load = def.spawn_on_load or false, + spawn_in_nodes = def.spawn_in_nodes or false, + spawn_cap = def.spawn_cap or 5 + } +end + +-- Mapgen -- + +minetest.register_node("creatura:spawn_node", { + drawtype = "airlike", + groups = {not_in_creative_inventory = 1}, + walkable = false +}) + +local mapgen_spawning = minetest.settings:get_bool("creatura_mapgen_spawning", true) +local mapgen_spawning_int = tonumber(minetest.settings:get("creatura_mapgen_spawn_interval")) or 64 + +if mapgen_spawning then + local chunk_delay = 0 + local c_air = minetest.get_content_id("air") + local c_spawn = minetest.get_content_id("creatura:spawn_node") + + minetest.register_on_generated(function(minp, maxp) + if chunk_delay > 0 then chunk_delay = chunk_delay - 1 end + local meta_queue = {} + + local vm, emin, emax = minetest.get_mapgen_object("voxelmanip") + local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax} + local data = vm:get_data() + + 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 def + local center + + local current_biome + local spawn_biomes + + local current_pos + + for _, mob_name in ipairs(mapgen_mobs) do + local mob_spawned = false + + def = creatura.registered_mob_spawns[mob_name] + + center = { + 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 + } + + current_biome = minetest.get_biome_name(minetest.get_biome_data(center).biome) + spawn_biomes = def.biomes + + if not mob_spawned + and (not spawn_biomes + or table_contains(spawn_biomes, current_biome)) then + for z = min_z + 8, max_z - 7, 8 do + if mob_spawned then break end + for x = min_x + 8, max_x - 7, 8 do + if mob_spawned then break end + for y = min_y, max_y do + local vi = area:index(x, y, z) + + if data[vi] == c_air + or data[vi] == c_spawn then + break + end + + -- Check if position is outside of vertical bounds + if y > def.max_height + or y < def.min_height then + break + end + + current_pos = vector.new(x, y, z) + + -- Check if position has required nodes + if not pos_meets_params(current_pos, def) then + break + end + + if def.spawn_in_nodes then + -- Add Spawn Node to Map + data[vi] = c_spawn + + local group_size = random(def.min_group or 1, def.max_group or 1) + table.insert(meta_queue, {pos = current_pos, mob = mob_name, cluster = group_size}) + + mob_spawned = true + break + elseif data[area:index(x, y + 1, z)] == c_air then + vi = area:index(x, y + 1, z) + current_pos = vector.new(x, y + 1, z) + + -- Add Spawn Node to Map + data[vi] = c_spawn + + local group_size = random(def.min_group or 1, def.max_group or 1) + table.insert(meta_queue, {pos = current_pos, mob = mob_name, cluster = group_size}) + + mob_spawned = true + break + end + end + end + end + end + end + + if #meta_queue > 0 then + vm:set_data(data) + vm:write_to_map() + + for _, unset_meta in ipairs(meta_queue) do + local pos = unset_meta.pos + local mob = unset_meta.mob + local cluster = unset_meta.cluster + + local meta = minetest.get_meta(pos) + meta:set_string("mob", mob) + meta:set_int("cluster", cluster) + end + + chunk_delay = mapgen_spawning_int + end + end) + + 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 plyr_found = false + local objects = minetest.get_objects_inside_radius(pos, abr) + + for _, object in ipairs(objects) do + if object:is_player() then + plyr_found = true + break + end + end + + if not plyr_found then return end + + 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) + do_on_spawn(pos, obj) + end + minetest.log("action", + "[Creatura] Spawned " .. amount .. " " .. name .. " at " .. minetest.pos_to_string(pos)) + else + obj = minetest.add_entity(pos, name) + do_on_spawn(pos, obj) + minetest.log("action", + "[Creatura] Spawned a " .. name .. " at " .. minetest.pos_to_string(pos)) + end + minetest.remove_node(pos) + end, + }) +end + +---------------- +-- DEPRECATED -- +---------------- + +function creatura.register_mob_spawn(name, def) + local spawn_def = { + 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_load = 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_def + + creatura.register_abm_spawn(name, spawn_def) +end + +function creatura.register_spawn_egg(name, col1, col2, inventory_image) + 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