--lua locals local S = minetest.get_translator("mcl_mobs") local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs local mob_class = mcl_mobs.mob_class local gamerule_doMobSpawning = true vl_tuning.setting("gamerule:doMobSpawning", "bool", { description = S("Whether mobs should spawn naturally, or via global spawning logic, such as for cats, phantoms, patrols, wandering traders, or zombie sieges. Does not affect special spawning attempts, like monster spawners, raids, or iron golems."), default = true, set = function(val) gamerule_doMobSpawning = val end, get = function() return gamerule_doMobSpawning end, }) local modern_lighting = minetest.settings:get_bool("mcl_mobs_modern_lighting", true) local nether_threshold = tonumber(minetest.settings:get("mcl_mobs_nether_threshold")) or 11 local end_threshold = tonumber(minetest.settings:get("mcl_mobs_end_threshold")) or 0 local overworld_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_threshold")) or 0 local overworld_sky_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_sky_threshold")) or 7 local overworld_passive_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_passive_threshold")) or 7 local get_node = minetest.get_node local get_node_light = minetest.get_node_light local find_nodes_in_area_under_air = minetest.find_nodes_in_area_under_air local mt_get_biome_name = minetest.get_biome_name local get_objects_inside_radius = minetest.get_objects_inside_radius local get_connected_players = minetest.get_connected_players local registered_nodes = minetest.registered_nodes local math_min = math.min local math_max = math.max local math_random = math.random local math_floor = math.floor local math_ceil = math.ceil local math_cos = math.cos local math_sin = math.sin local math_sqrt = math.sqrt local math_abs = math.abs local vector_distance = vector.distance local vector_new = vector.new local vector_floor = vector.floor local table_copy = table.copy local table_remove = table.remove local pairs = pairs local logging = minetest.settings:get_bool("mcl_logging_mobs_spawn", false) local function mcl_log(message, property) if property then message = message .. ": " .. dump(property) end mcl_util.mcl_log(message, "[Mobs spawn]", true) end if not logging then mcl_log = function() end end local dbg_spawn_attempts = 0 local dbg_spawn_succ = 0 local remove_far = true local WAIT_FOR_SPAWN_ATTEMPT = 10 local FIND_SPAWN_POS_RETRIES = 16 local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8 local MOB_SPAWN_ZONE_INNER = 24 local MOB_SPAWN_ZONE_INNER_SQ = MOB_SPAWN_ZONE_INNER^2 -- squared local MOB_SPAWN_ZONE_MIDDLE = 32 local MOB_SPAWN_ZONE_OUTER = 128 local MOB_SPAWN_ZONE_OUTER_SQ = MOB_SPAWN_ZONE_OUTER^2 -- squared -- range for mob count local MOB_CAP_INNER_RADIUS = 32 local aoc_range = 136 local MISSING_CAP_DEFAULT = 15 local MOBS_CAP_CLOSE = 10 local SPAWN_MAPGEN_LIMIT = mcl_vars.mapgen_limit - 150 local mob_cap = { hostile = tonumber(minetest.settings:get("mcl_mob_cap_monster")) or 70, passive = tonumber(minetest.settings:get("mcl_mob_cap_animal")) or 10, ambient = tonumber(minetest.settings:get("mcl_mob_cap_ambient")) or 15, water = tonumber(minetest.settings:get("mcl_mob_cap_water")) or 8, water_ambient = tonumber(minetest.settings:get("mcl_mob_cap_water_ambient")) or 20, water_underground = tonumber(minetest.settings:get("mcl_mob_cap_water_underground")) or 5, axolotl = tonumber(minetest.settings:get("mcl_mob_cap_axolotl")) or 2, -- TODO should be 5 when lush caves added player = tonumber(minetest.settings:get("mcl_mob_cap_player")) or 75, global_hostile = tonumber(minetest.settings:get("mcl_mob_cap_hostile")) or 300, global_non_hostile = tonumber(minetest.settings:get("mcl_mob_cap_non_hostile")) or 300, total = tonumber(minetest.settings:get("mcl_mob_cap_total")) or 500, } local peaceful_percentage_spawned = tonumber(minetest.settings:get("mcl_mob_peaceful_percentage_spawned")) or 30 local peaceful_group_percentage_spawned = tonumber(minetest.settings:get("mcl_mob_peaceful_group_percentage_spawned")) or 15 local hostile_group_percentage_spawned = tonumber(minetest.settings:get("mcl_mob_hostile_group_percentage_spawned")) or 20 mcl_log("Mob cap hostile: " .. mob_cap.hostile) mcl_log("Mob cap water: " .. mob_cap.water) mcl_log("Mob cap passive: " .. mob_cap.passive) mcl_log("Percentage of peacefuls spawned: " .. peaceful_percentage_spawned) mcl_log("Percentage of peaceful spawns are group: " .. peaceful_group_percentage_spawned) mcl_log("Percentage of hostile spawns are group: " .. hostile_group_percentage_spawned) --do mobs spawn? local mobs_spawn = minetest.settings:get_bool("mobs_spawn", true) ~= false local spawn_protected = minetest.settings:get_bool("mobs_spawn_protected") ~= false -- count how many mobs are in an area local function count_mobs(pos,r,mob_type) local num = 0 for _,l in pairs(minetest.luaentities) do if l and l.is_mob and (mob_type == nil or l.type == mob_type) then local p = l.object:get_pos() if p and vector_distance(p,pos) < r then num = num + 1 end end end return num end local function count_mobs_total(mob_type) local num = 0 for _,l in pairs(minetest.luaentities) do if l.is_mob then if mob_type == nil or l.type == mob_type then num = num + 1 end end end return num end local function count_mobs_add_entry (mobs_list, mob_cat) mobs_list[mob_cat] = (mobs_list[mob_cat] or 0) + 1 end --categorise_by can be name or type or spawn_class local function count_mobs_all(categorise_by, pos) local mobs_found_wide = {} local mobs_found_close = {} local num = 0 for _,entity in pairs(minetest.luaentities) do if entity and entity.is_mob then local add_entry = false --local mob_type = entity.type -- animal / monster / npc local mob_cat = entity[categorise_by] if pos then local mob_pos = entity.object:get_pos() if mob_pos then local distance = vector.distance(pos, mob_pos) --mcl_log("distance: ".. distance) if distance <= MOB_SPAWN_ZONE_MIDDLE then --mcl_log("distance is close") count_mobs_add_entry (mobs_found_close, mob_cat) count_mobs_add_entry (mobs_found_wide, mob_cat) add_entry = true elseif distance <= MOB_SPAWN_ZONE_OUTER then --mcl_log("distance is wide") count_mobs_add_entry (mobs_found_wide, mob_cat) add_entry = true else --mcl_log("mob_pos: " .. minetest.pos_to_string(mob_pos)) end end else count_mobs_add_entry (mobs_found_wide, mob_cat) add_entry = true end if add_entry then num = num + 1 end end end --mcl_log("num: ".. num) return mobs_found_close, mobs_found_wide, num end local function count_mobs_total_cap(mob_type) local total = 0 local num = 0 local hostile = 0 local non_hostile = 0 for _,l in pairs(minetest.luaentities) do if l.is_mob then total = total + 1 local nametagged = l.nametag and l.nametag ~= "" if ( mob_type == nil or l.type == mob_type ) and not nametagged then if l.spawn_class == "hostile" then hostile = hostile + 1 else non_hostile = non_hostile + 1 end num = num + 1 else mcl_log("l.name", l.name) mcl_log("l.nametag", l.nametag) end end end mcl_log("Total mobs", total) mcl_log("hostile", hostile) mcl_log("non_hostile", non_hostile) return num, non_hostile, hostile end local function output_mob_stats(mob_counts, total_mobs, chat_display) if (total_mobs) then local total_output = "Total mobs found: " .. total_mobs if chat_display then minetest.log(total_output) else minetest.log("action", total_output) end end local detailed = "" if mob_counts then for k, v1 in pairs(mob_counts) do detailed = detailed .. tostring(k) .. ": " .. tostring(v1) .. "; " end end if detailed and detailed ~= "" then if chat_display then minetest.log(detailed) else minetest.log("action", detailed) end end end -- global functions function mcl_mobs:spawn_abm_check(pos, node, name) -- global function to add additional spawn checks -- return true to stop spawning mob end --[[ Custom elements changed: name: the mobs name dimension: "overworld" "nether" "end" types of spawning: "water" "ground" "lava" biomes: tells the spawner to allow certain mobs to spawn in certain biomes {"this", "that", "grasslands", "whatever"} what is aoc??? objects in area WARNING: BIOME INTEGRATION NEEDED -> How to get biome through lua?? ]]-- --this is where all of the spawning information is kept local spawn_dictionary = {} --this is where all of the spawning information is kept for mobs that don't naturally spawn local non_spawn_dictionary = {} function mcl_mobs:spawn_setup(def) if not mobs_spawn then return end if not def then minetest.log("warning", "Empty mob spawn setup definition") return end local name = def.name if not name then minetest.log("warning", "Missing mob name") return end local dimension = def.dimension or "overworld" local type_of_spawning = def.type_of_spawning or "ground" local biomes = def.biomes or nil local min_light = def.min_light or 0 local max_light = def.max_light or (minetest.LIGHT_MAX + 1) local chance = def.chance or 1000 local aoc = def.aoc or aoc_range local min_height = def.min_height or mcl_mapgen.overworld.min local max_height = def.max_height or mcl_mapgen.overworld.max local day_toggle = def.day_toggle local on_spawn = def.on_spawn local check_position = def.check_position -- chance/spawn number override in minetest.conf for registered mob local numbers = minetest.settings:get(name) if numbers then numbers = numbers:split(",") chance = tonumber(numbers[1]) or chance aoc = tonumber(numbers[2]) or aoc if chance == 0 then minetest.log("warning", string.format("[mcl_mobs] %s has spawning disabled", name)) return end minetest.log("action", string.format("[mcl_mobs] Chance setting for %s changed to %s (total: %s)", name, chance, aoc)) end if chance < 1 then chance = 1 minetest.log("warning", "Chance shouldn't be less than 1 (mob name: " .. name ..")") end spawn_dictionary[#spawn_dictionary + 1] = { name = name, dimension = dimension, type_of_spawning = type_of_spawning, biomes = biomes, min_light = min_light, max_light = max_light, chance = chance, aoc = aoc, min_height = min_height, max_height = max_height, day_toggle = day_toggle, check_position = check_position, on_spawn = on_spawn, } end function mcl_mobs:mob_light_lvl(mob_name, dimension) local spawn_dictionary_consolidated = {} if non_spawn_dictionary[mob_name] then local mob_dimension = non_spawn_dictionary[mob_name][dimension] if mob_dimension then --minetest.log("Found in non spawn dictionary for dimension") return mob_dimension.min_light, mob_dimension.max_light else --minetest.log("Found in non spawn dictionary but not for dimension") local overworld_non_spawn_def = non_spawn_dictionary[mob_name]["overworld"] if overworld_non_spawn_def then return overworld_non_spawn_def.min_light, overworld_non_spawn_def.max_light end end else --minetest.log("must be in spawning dictionary") for i,v in pairs(spawn_dictionary) do local current_mob_name = spawn_dictionary[i].name local current_mob_dim = spawn_dictionary[i].dimension if mob_name == current_mob_name then if not spawn_dictionary_consolidated[current_mob_name] then spawn_dictionary_consolidated[current_mob_name] = {} end spawn_dictionary_consolidated[current_mob_name][current_mob_dim] = { ["min_light"] = spawn_dictionary[i].min_light, ["max_light"] = spawn_dictionary[i].max_light } end end if spawn_dictionary_consolidated[mob_name] then --minetest.log("is in consolidated") local mob_dimension = spawn_dictionary_consolidated[mob_name][dimension] if mob_dimension then --minetest.log("found for dimension") return mob_dimension.min_light, mob_dimension.max_light else --minetest.log("not found for dimension, use overworld def") local mob_dimension_default = spawn_dictionary_consolidated[mob_name]["overworld"] if mob_dimension_default then return mob_dimension_default.min_light, mob_dimension_default.max_light end end else --minetest.log("not in consolidated") end end minetest.log("action", "There are no light levels for mob (" .. tostring(mob_name) .. ") in dimension (" .. tostring(dimension) .. "). Return defaults") return 0, minetest.LIGHT_MAX+1 end function mcl_mobs:non_spawn_specific(mob_name,dimension,min_light,max_light) table.insert(non_spawn_dictionary, mob_name) non_spawn_dictionary[mob_name] = { [dimension] = { min_light = min_light , max_light = max_light } } end function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_light, max_light, interval, chance, aoc, min_height, max_height, day_toggle, on_spawn, check_position) -- Do mobs spawn at all? if not mobs_spawn then return end assert(min_height) assert(max_height) -- chance/spawn number override in minetest.conf for registered mob local numbers = minetest.settings:get(name) if numbers then numbers = numbers:split(",") chance = tonumber(numbers[1]) or chance aoc = tonumber(numbers[2]) or aoc if chance == 0 then minetest.log("warning", string.format("[mcl_mobs] %s has spawning disabled", name)) return end minetest.log("action", string.format("[mcl_mobs] Chance setting for %s changed to %s (total: %s)", name, chance, aoc)) end --load information into the spawn dictionary local key = #spawn_dictionary + 1 spawn_dictionary[key] = {} spawn_dictionary[key]["name"] = name spawn_dictionary[key]["dimension"] = dimension spawn_dictionary[key]["type_of_spawning"] = type_of_spawning spawn_dictionary[key]["biomes"] = biomes spawn_dictionary[key]["min_light"] = min_light spawn_dictionary[key]["max_light"] = max_light spawn_dictionary[key]["chance"] = chance spawn_dictionary[key]["aoc"] = aoc spawn_dictionary[key]["min_height"] = min_height spawn_dictionary[key]["max_height"] = max_height spawn_dictionary[key]["day_toggle"] = day_toggle spawn_dictionary[key]["check_position"] = check_position end local function get_next_mob_spawn_pos(pos) -- Select a distance such that distances closer to the player are selected much more often than -- those further away from the player. This does produce a concentration at INNER (24 blocks) local distance = math_random()^2 * (MOB_SPAWN_ZONE_OUTER - MOB_SPAWN_ZONE_INNER) + MOB_SPAWN_ZONE_INNER --print("Using spawn distance of "..tostring(distance).." fx="..tostring(fx)..",x="..tostring(x)) -- Choose a random direction. Rejection sampling is simple and fast (1-2 tries usually) local xoff, yoff, zoff, dd repeat xoff, yoff, zoff = math_random() * 2 - 1, math_random() * 2 - 1, math_random() * 2 - 1 dd = xoff*xoff + yoff*yoff + zoff*zoff until (dd <= 1 and dd >= 1e-6) -- outside of uniform ball, retry dd = distance / math_sqrt(dd) -- distance scaling factor xoff, yoff, zoff = xoff * dd, yoff * dd, zoff * dd local goal_pos = vector.offset(pos, xoff, yoff, zoff) if not (math_abs(goal_pos.x) <= SPAWN_MAPGEN_LIMIT and math_abs(goal_pos.y) <= SPAWN_MAPGEN_LIMIT and math_abs(goal_pos.z) <= SPAWN_MAPGEN_LIMIT) then mcl_log("Pos outside mapgen limits: " .. minetest.pos_to_string(goal_pos)) return nil end -- Calculate upper/lower y limits local d2 = xoff*xoff + zoff*zoff -- squared distance in x,z plane only local y1 = math_sqrt(MOB_SPAWN_ZONE_OUTER_SQ - d2) -- absolue value of distance to outer sphere local y_min, y_max if d2 >= MOB_SPAWN_ZONE_INNER_SQ then -- Outer region, y range has both ends on the outer sphere y_min = pos.y - y1 y_max = pos.y + y1 else -- Inner region, y range spans between inner and outer spheres local y2 = math_sqrt(MOB_SPAWN_ZONE_INNER_SQ - d2) if goal_pos.y > pos.y then -- Upper hemisphere y_min = pos.y + y2 y_max = pos.y + y1 else -- Lower hemisphere y_min = pos.y - y1 y_max = pos.y - y2 end end -- Limit total range of check to 32 nodes (maximum of 3 map blocks) y_min = math_max(math_floor(y_min), goal_pos.y - 16) y_max = math_min(math_ceil(y_max), goal_pos.y + 16) -- Ask engine for valid spawn locations local spawning_position_list = find_nodes_in_area_under_air( {x = goal_pos.x, y = y_min, z = goal_pos.z}, {x = goal_pos.x, y = y_max, z = goal_pos.z}, {"group:solid", "group:water", "group:lava"} ) or {} -- Select only the locations at a valid distance local valid_positions = {} for _,check_pos in ipairs(spawning_position_list) do local dist = vector.distance(pos, check_pos) if dist >= MOB_SPAWN_ZONE_INNER and dist <= MOB_SPAWN_ZONE_OUTER then valid_positions[#valid_positions + 1] = check_pos end end spawning_position_list = valid_positions -- No valid locations, failed to find a position if #spawning_position_list == 0 then mcl_log("Spawning position isn't good. Do not spawn: " .. minetest.pos_to_string(goal_pos)) return nil end -- Pick a random valid location mcl_log("Spawning positions available: " .. minetest.pos_to_string(goal_pos)) return spawning_position_list[math_random(1, #spawning_position_list)] end --a simple helper function for mob_spawn local function biome_check(biome_list, biome_goal) if not biome_goal then return false end for _, data in pairs(biome_list) do if data == biome_goal then return true end end return false end local function is_farm_animal(n) return n == "mobs_mc:pig" or n == "mobs_mc:cow" or n == "mobs_mc:sheep" or n == "mobs_mc:chicken" or n == "mobs_mc:horse" or n == "mobs_mc:donkey" end local function get_water_spawn(p) local nn = minetest.find_nodes_in_area(vector.offset(p,-2,-1,-2),vector.offset(p,2,-15,2),{"group:water"}) if nn and #nn > 0 then return nn[math.random(#nn)] end end local function has_room(self, pos) local cb = self.spawnbox or self.collisionbox local nodes = {} if self.fly_in then local t = type(self.fly_in) if t == "table" then nodes = table.copy(self.fly_in) elseif t == "string" then table.insert(nodes,self.fly_in) end end table.insert(nodes,"air") -- Calculate area to check for room local cb_height = cb[5] - cb[2] local p1 = vector.new( math.round(pos.x + cb[1]), math.floor(pos.y), math.round(pos.z + cb[3])) local p2 = vector.new( math.round(pos.x + cb[4]), math.ceil(p1.y + cb_height) - 1, math.round(pos.z + cb[6])) -- Check if the entire spawn volume is free local dx = p2.x - p1.x + 1 local dy = p2.y - p1.y + 1 local dz = p2.z - p1.z + 1 local found_nodes = minetest.find_nodes_in_area(p1,p2,nodes) or 0 local n = #found_nodes if n == dx * dy * dz then return true end -- If we don't have an implementation of get_node_boxes, we can't check for sub-node space if not minetest.get_node_boxes then return false end -- Check if it's possible for a sub-node space check to succeed local needed_in_bottom_section = dx * ( dy - 1) * dz if n < needed_in_bottom_section then return false end -- Make sure the entire volume except for the top level is free before checking the top layer if dy > 1 then -- Remove nodes in the top layer from the count for i = 1,#found_nodes do if found_nodes[i].y == p2.y then n = n - 1 end end -- If the entire volume except the top layer isn't air (or nodes) then we can't spawn this mob here if n < needed_in_bottom_section then return false end end -- Check the top layer to see if we have enough space to spawn in local top_layer_height = 1 local processed = {} for x = p1.x,p2.x do for z = p1.z,p2.z do local test_pos = vector.new(x,p2.y,z) local node = minetest.get_node(test_pos) or { name = "ignore" } local cache_name = string.format("%s-%d", node.name, node.param2) if not processed[cache_name] then -- Calculate node bounding box and select the lowest y value local boxes = minetest.get_node_boxes("collision_box", test_pos, node) for i = 1,#boxes do local box = boxes[i] local y_test = box[2] + 0.5 if y_test < top_layer_height then top_layer_height = y_test end local y_test = box[5] + 0.5 if y_test < top_layer_height then top_layer_height = y_test end end end end end if top_layer_height + dy - 1 >= cb_height then return true end -- We don't have room return false end mcl_mobs.custom_biomecheck = nil function mcl_mobs.register_custom_biomecheck(custom_biomecheck) mcl_mobs.custom_biomecheck = custom_biomecheck end local function get_biome_name(pos) if mcl_mobs.custom_biomecheck then return mcl_mobs.custom_biomecheck(pos) end local gotten_biome = minetest.get_biome_data(pos) return gotten_biome and mt_get_biome_name(gotten_biome.biome) end local function spawn_check(pos, spawn_def) if not spawn_def or not pos then return end dbg_spawn_attempts = dbg_spawn_attempts + 1 local dimension = mcl_worlds.pos_to_dimension(pos) if spawn_def.dimension ~= dimension then return end -- wrong dimension -- find ground node below spawn position local node_name = get_node(pos).name local node_def = registered_nodes[node_name] if node_def and not node_def.groups.solid then -- try node one below instead pos.y = pos.y - 1 node_name = get_node(pos).name node_def = registered_nodes[node_name] end if not node_def or not node_def.groups then return end -- do not spawn on bedrock if node_name == "mcl_core:bedrock" then return end pos.y = pos.y + 1 -- check spawn height if pos.y < spawn_def.min_height or pos.y > spawn_def.max_height then return end mcl_log("spawn_check#1 position checks passed") -- do not spawn ground mobs on leaves if spawn_def.type_of_spawning == "ground" and (not node_def.groups.solid or node_def.groups.leaves) then return end -- water mobs only on water if spawn_def.type_of_spawning == "water" and not node_def.groups.water then return end -- lava mobs only on lava if spawn_def.type_of_spawning == "lava" and not node_def.groups.lava then return end -- farm animals on grass only if is_farm_animal(spawn_def.name) and not node_def.groups.grass_block then return end ---- More expensive calls: -- check the biome if spawn_def.biomes and not biome_check(spawn_def.biomes, get_biome_name(pos)) then return end -- check if there is enough room local mob_def = minetest.registered_entities[spawn_def.name] if not has_room(mob_def,pos) then return end -- additional checks (slime etc.) if spawn_def.check_position and not spawn_def.check_position(pos) then return end if spawn_protected and minetest.is_protected(pos, "") then return end mcl_log("spawn_check#2 advanced checks passed") -- check light thresholds local gotten_light = get_node_light(pos) -- old lighting if not modern_lighting then return gotten_light >= spawn_def.min_light and gotten_light <= spawn_def.max_light end local sky_light = minetest.get_natural_light(pos) local art_light = minetest.get_artificial_light(get_node(pos).param1) if mob_def.spawn_check then return mob_def.spawn_check(pos, gotten_light, art_light, sky_light) end if mob_def.type == "monster" then if dimension == "nether" then if art_light <= nether_threshold then return true end elseif dimension == "end" then if art_light <= end_threshold then return true end elseif dimension == "overworld" then if art_light <= overworld_threshold and sky_light <= overworld_sky_threshold then return true end end return false end -- passive threshold is apparently the same in all dimensions ... return gotten_light > overworld_passive_threshold end function mcl_mobs.spawn(pos,id) if not pos or not id then return false end local def = minetest.registered_entities[id] or minetest.registered_entities["mobs_mc:"..id] or minetest.registered_entities["extra_mobs:"..id] if not def or not def.is_mob or (def.can_spawn and not def.can_spawn(pos)) then return false end if not has_room(def, pos) then return false end return minetest.add_entity(pos, def.name) end local function spawn_group(p,mob,spawn_on,amount_to_spawn) local nn= minetest.find_nodes_in_area_under_air(vector.offset(p,-5,-3,-5),vector.offset(p,5,3,5),spawn_on) local o table.shuffle(nn) if not nn or #nn < 1 then nn = {} table.insert(nn,p) end for i = 1, amount_to_spawn do local sp = vector.offset(nn[math.random(#nn)],0,1,0) if spawn_check(nn[math.random(#nn)],mob) then if mob.type_of_spawning == "water" then sp = get_water_spawn(sp) end o = mcl_mobs.spawn(sp,mob.name) if o then dbg_spawn_succ = dbg_spawn_succ + 1 end end end return o end mcl_mobs.spawn_group = spawn_group minetest.register_chatcommand("spawn_mob",{ privs = { debug = true }, description=S("spawn_mob is a chatcommand that allows you to type in the name of a mob without 'typing mobs_mc:' all the time like so; 'spawn_mob spider'. however, there is more you can do with this special command, currently you can edit any number, boolean, and string variable you choose with this format: spawn_mob 'any_mob:var:'. any_mob being your mob of choice, mobs_variable being the variable, and variable value being the value of the chosen variable. and example of this format: \n spawn_mob skeleton:var:\n this would spawn a skeleton that wouldn't attack you. REMEMBER-THIS> when changing a number value always prefix it with 'NUM', example: \n spawn_mob skeleton:var:\n this setting the skelly's jump height to 10. if you want to make multiple changes to a mob, you can, example: \n spawn_mob skeleton:var::var::var::var:\n etc."), func = function(n,param) local pos = minetest.get_player_by_name(n):get_pos() local modifiers = {} for capture in string.gmatch(param, "%:(.-)%:") do table.insert(modifiers, ":"..capture) end local mod1 = string.find(param, ":") local mobname = param if mod1 then mobname = string.sub(param, 1, mod1-1) end local mob = mcl_mobs.spawn(pos,mobname) if mob then for c=1, #modifiers do modifs = modifiers[c] local mod1 = string.find(modifs, ":") local mod_start = string.find(modifs, "<") local mod_vals = string.find(modifs, "=") local mod_end = string.find(modifs, ">") local mob_entity = mob:get_luaentity() if string.sub(modifs, mod1+1, mod1+3) == "var" then if mod1 and mod_start and mod_vals and mod_end then local variable = string.sub(modifs, mod_start+1, mod_vals-1) local value = string.sub(modifs, mod_vals+1, mod_end-1) number_tag = string.find(value, "NUM") if number_tag then value = tonumber(string.sub(value, 4, -1)) end if value == "true" then value = true elseif value == "false" then value = false end if not mob_entity[variable] then minetest.log("warning", n.." mob variable "..variable.." previously unset") end mob_entity[variable] = value else minetest.log("warning", n.." couldn't modify "..mobname.." at "..minetest.pos_to_string(pos).. ", missing paramaters") end else minetest.log("warning", n.." couldn't modify "..mobname.." at "..minetest.pos_to_string(pos).. ", missing modification type") end end minetest.log("action", n.." spawned "..mobname.." at "..minetest.pos_to_string(pos)) return true, mobname.." spawned at "..minetest.pos_to_string(pos) else return false, "Couldn't spawn "..mobname end end }) if mobs_spawn then -- Get pos to spawn, x and z are randomised, y is range local function mob_cap_space (pos, mob_type, mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile) -- Some mob examples --type = "monster", spawn_class = "hostile", --type = "animal", spawn_class = "passive", --local cod = { type = "animal", spawn_class = "water", local type_cap = mob_cap[mob_type] or MISSING_CAP_DEFAULT local close_zone_cap = MOBS_CAP_CLOSE local mob_total_wide = mob_counts_wide[mob_type] if not mob_total_wide then --mcl_log("none of type found. set as 0") mob_total_wide = 0 end local cap_space_wide = math_max(type_cap - mob_total_wide, 0) mcl_log("mob_type", mob_type) mcl_log("cap_space_wide", cap_space_wide) local cap_space_available = 0 if mob_type == "hostile" then mcl_log("cap_space_global", cap_space_hostile) cap_space_available = math_min(cap_space_hostile, cap_space_wide) else mcl_log("cap_space_global", cap_space_non_hostile) cap_space_available = math_min(cap_space_non_hostile, cap_space_wide) end local mob_total_close = mob_counts_close[mob_type] if not mob_total_close then --mcl_log("none of type found. set as 0") mob_total_close = 0 end local cap_space_close = math_max(close_zone_cap - mob_total_close, 0) cap_space_available = math_min(cap_space_available, cap_space_close) mcl_log("cap_space_close", cap_space_close) mcl_log("cap_space_available", cap_space_available) if false and mob_type == "water" then mcl_log("mob_type: " .. mob_type .. " and pos: " .. minetest.pos_to_string(pos)) mcl_log("wide: " .. mob_total_wide .. "/" .. type_cap) mcl_log("cap_space_wide: " .. cap_space_wide) mcl_log("close: " .. mob_total_close .. "/" .. close_zone_cap) mcl_log("cap_space_close: " .. cap_space_close) end return cap_space_available end local function find_spawning_position(pos, max_times) local spawning_position local max_loops = max_times or 1 --mcl_log("mapgen_limit: " .. SPAWN_MAPGEN_LIMIT) while max_loops > 0 do local spawning_position = get_next_mob_spawn_pos(pos) if spawning_position then return spawning_position end max_loops = max_loops - 1 end return nil end local cumulative_chance = nil local mob_library_worker_table = nil local function initialize_spawn_data() if not mob_library_worker_table then mob_library_worker_table = table_copy(spawn_dictionary) end if not cumulative_chance then cumulative_chance = 0 for k, v in pairs(mob_library_worker_table) do cumulative_chance = cumulative_chance + v.chance end end end local function spawn_a_mob(pos, cap_space_hostile, cap_space_non_hostile) local spawning_position = find_spawning_position(pos, FIND_SPAWN_POS_RETRIES) if not spawning_position then minetest.log("action", "[Mobs spawn] Cannot find a valid spawn position after retries: " .. FIND_SPAWN_POS_RETRIES) return end local mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("spawn_class", spawning_position) --output_mob_stats(mob_counts_close, total_mobs) --output_mob_stats(mob_counts_wide) --grab mob that fits into the spawning location --use random weighted choice with replacement to grab a mob, don't exclude any possibilities --shuffle table once every loop to provide equal inclusion probability to all mobs --repeat grabbing a mob to maintain existing spawn rates local spawn_loop_counter = #mob_library_worker_table while spawn_loop_counter > 0 do table.shuffle(mob_library_worker_table) local mob_chance_offset = math_random(1, cumulative_chance) local mob_index = 1 local mob_chance = mob_library_worker_table[mob_index].chance local step_chance = mob_chance while step_chance < mob_chance_offset do mob_index = mob_index + 1 if mob_index <= #mob_library_worker_table then mob_chance = mob_library_worker_table[mob_index].chance step_chance = step_chance + mob_chance else break end end --minetest.log(mob_def.name.." "..step_chance.. " "..mob_chance) local mob_def = mob_library_worker_table[mob_index] if mob_def and mob_def.name and minetest.registered_entities[mob_def.name] then local mob_def_ent = minetest.registered_entities[mob_def.name] local mob_spawn_class = mob_def_ent.spawn_class local cap_space_available = mob_cap_space (spawning_position, mob_spawn_class, mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile) if cap_space_available > 0 then --mcl_log("Cap space available") -- Spawn caps for animals and water creatures fill up rapidly. Need to throttle this somewhat -- for performance and for early game challenge. We don't want to reduce hostiles though. local spawn_hostile = (mob_spawn_class == "hostile") local spawn_passive = (mob_spawn_class ~= "hostile") and math.random(100) < peaceful_percentage_spawned --mcl_log("Spawn_passive: " .. tostring(spawn_passive)) --mcl_log("Spawn_hostile: " .. tostring(spawn_hostile)) if (spawn_hostile or spawn_passive) and spawn_check(spawning_position,mob_def) then if mob_def.type_of_spawning == "water" then spawning_position = get_water_spawn(spawning_position) if not spawning_position then minetest.log("warning","[mcl_mobs] no water spawn for mob "..mob_def.name.." found at "..minetest.pos_to_string(vector.round(pos))) return end end if mob_def_ent.can_spawn and not mob_def_ent.can_spawn(spawning_position) then minetest.log("warning","[mcl_mobs] mob "..mob_def.name.." refused to spawn at "..minetest.pos_to_string(vector.round(spawning_position))) return end --everything is correct, spawn mob local spawn_in_group = mob_def_ent.spawn_in_group or 4 local spawn_group_hostile = (mob_spawn_class == "hostile") and (math.random(100) < hostile_group_percentage_spawned) local spawn_group_passive = (mob_spawn_class ~= "hostile") and (math.random(100) < peaceful_group_percentage_spawned) mcl_log("spawn_group_hostile: " .. tostring(spawn_group_hostile)) mcl_log("spawn_group_passive: " .. tostring(spawn_group_passive)) local spawned if spawn_in_group and (spawn_group_hostile or spawn_group_passive) then local group_min = mob_def_ent.spawn_in_group_min or 1 if not group_min then group_min = 1 end local amount_to_spawn = math.random(group_min, spawn_in_group) mcl_log("Spawning quantity: " .. amount_to_spawn) amount_to_spawn = math_min(amount_to_spawn, cap_space_available) mcl_log("throttled spawning quantity: " .. amount_to_spawn) if logging then minetest.log("action", "[mcl_mobs] A group of " ..amount_to_spawn .. " " .. mob_def.name .. " mob spawns on " ..minetest.get_node(vector.offset(spawning_position,0,-1,0)).name .." at " .. minetest.pos_to_string(spawning_position, 1)) end spawned = spawn_group(spawning_position,mob_def,{minetest.get_node(vector.offset(spawning_position,0,-1,0)).name}, amount_to_spawn) else if logging then minetest.log("action", "[mcl_mobs] Mob " .. mob_def.name .. " spawns on " ..minetest.get_node(vector.offset(spawning_position,0,-1,0)).name .." at ".. minetest.pos_to_string(spawning_position, 1)) end spawned = mcl_mobs.spawn(spawning_position, mob_def.name) end if spawned then --mcl_log("We have spawned") mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("spawn_class", pos) local new_spawning_position = find_spawning_position(pos, FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN) if new_spawning_position then mcl_log("Setting new spawning position") spawning_position = new_spawning_position else mcl_log("Cannot set new spawning position") end end else --mcl_log("Spawn check failed") end else --mcl_log("Cap space full") end end spawn_loop_counter = spawn_loop_counter - 1 end end --MAIN LOOP local timer = 0 minetest.register_globalstep(function(dtime) if not gamerule_doMobSpawning then return end timer = timer + dtime if timer < WAIT_FOR_SPAWN_ATTEMPT then return end initialize_spawn_data() timer = 0 local players = get_connected_players() local total_mobs, total_non_hostile, total_hostile = count_mobs_total_cap() local cap_space_hostile = math_max(mob_cap.global_hostile - total_hostile, 0) local cap_space_non_hostile = math_max(mob_cap.global_non_hostile - total_non_hostile, 0) mcl_log("global cap_space_hostile", cap_space_hostile) mcl_log("global cap_space_non_hostile", cap_space_non_hostile) if total_mobs > mob_cap.total or total_mobs > #players * mob_cap.player then minetest.log("action","[mcl_mobs] global mob cap reached. no cycle spawning.") return end --mob cap per player for _, player in pairs(players) do local pos = player:get_pos() local dimension = mcl_worlds.pos_to_dimension(pos) -- ignore void and unloaded area if dimension ~= "void" and dimension ~= "default" then spawn_a_mob(pos, cap_space_hostile, cap_space_non_hostile) end end end) end local function despawn_allowed(self) local nametag = self.nametag and self.nametag ~= "" local not_busy = self.state ~= "attack" and self.following == nil if self.can_despawn == true then if not nametag and not_busy and not self.tamed == true and not self.persistent == true then return true end end return false end function mob_class:despawn_allowed() despawn_allowed(self) end assert(despawn_allowed({can_despawn=false}) == false, "despawn_allowed - can_despawn false failed") assert(despawn_allowed({can_despawn=true}) == true, "despawn_allowed - can_despawn true failed") assert(despawn_allowed({can_despawn=true, nametag=""}) == true, "despawn_allowed - blank nametag failed") assert(despawn_allowed({can_despawn=true, nametag=nil}) == true, "despawn_allowed - nil nametag failed") assert(despawn_allowed({can_despawn=true, nametag="bob"}) == false, "despawn_allowed - nametag failed") assert(despawn_allowed({can_despawn=true, state="attack"}) == false, "despawn_allowed - attack state failed") assert(despawn_allowed({can_despawn=true, following="blah"}) == false, "despawn_allowed - following state failed") assert(despawn_allowed({can_despawn=true, tamed=false}) == true, "despawn_allowed - not tamed") assert(despawn_allowed({can_despawn=true, tamed=true}) == false, "despawn_allowed - tamed") assert(despawn_allowed({can_despawn=true, persistent=true}) == false, "despawn_allowed - persistent") assert(despawn_allowed({can_despawn=true, persistent=false}) == true, "despawn_allowed - not persistent") function mob_class:check_despawn(pos, dtime) self.lifetimer = self.lifetimer - dtime -- Despawning: when lifetimer expires, remove mob if remove_far and despawn_allowed(self) then if self.despawn_immediately or self.lifetimer <= 0 then if logging then minetest.log("action", "[mcl_mobs] Mob "..self.name.." despawns at "..minetest.pos_to_string(pos, 1) .. " lifetimer ran out") end mcl_burning.extinguish(self.object) self.object:remove() return true elseif self.lifetimer <= 10 then if math.random(10) < 4 then self.despawn_immediately = true else self.lifetimer = 20 end end end end minetest.register_chatcommand("mobstats",{ privs = { debug = true }, func = function(n,param) local pos = minetest.get_player_by_name(n):get_pos() minetest.chat_send_player(n,"mobs: within 32 radius of player/total loaded :"..count_mobs(pos,MOB_CAP_INNER_RADIUS) .. "/" .. count_mobs_total()) minetest.chat_send_player(n,"spawning attempts since server start:" .. dbg_spawn_succ .. "/" .. dbg_spawn_attempts) local mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("name") -- Can use "type" output_mob_stats(mob_counts_wide, total_mobs, true) end })