2021-04-08 15:52:50 +02:00
--lua locals
2022-11-11 00:21:47 +01:00
local math , vector , minetest , mcl_mobs = math , vector , minetest , mcl_mobs
local mob_class = mcl_mobs.mob_class
2023-09-19 03:18:40 +02:00
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
2023-09-20 01:24:51 +02:00
local overworld_passive_threshold = tonumber ( minetest.settings : get ( " mcl_mobs_overworld_passive_threshold " ) ) or 7
2024-06-30 17:12:12 +02:00
local debug_time_threshold = tonumber ( minetest.settings : get ( " vl_debug_time_threshold " ) ) or 1000
2023-09-19 03:18:40 +02:00
2022-04-15 11:48:58 +02:00
local get_node = minetest.get_node
local get_item_group = minetest.get_item_group
local get_node_light = minetest.get_node_light
2021-04-08 15:52:50 +02:00
local find_nodes_in_area_under_air = minetest.find_nodes_in_area_under_air
2024-04-01 17:40:14 +02:00
local mt_get_biome_name = minetest.get_biome_name
2022-04-15 11:48:58 +02:00
local get_objects_inside_radius = minetest.get_objects_inside_radius
local get_connected_players = minetest.get_connected_players
2024-07-31 02:30:29 +02:00
local math_min = math.min
local math_max = math.max
2021-04-11 00:10:27 +02:00
local math_random = math.random
2022-04-15 11:48:58 +02:00
local math_floor = math.floor
local math_ceil = math.ceil
local math_cos = math.cos
local math_sin = math.sin
2024-03-18 19:20:46 +01:00
local math_sqrt = math.sqrt
2022-04-15 11:48:58 +02:00
2022-07-17 05:42:45 +02:00
local vector_distance = vector.distance
2022-04-15 11:48:58 +02:00
local vector_new = vector.new
local vector_floor = vector.floor
local table_copy = table.copy
local table_remove = table.remove
local pairs = pairs
2023-01-26 04:57:36 +01:00
2024-08-03 17:52:51 +02:00
local logging = minetest.settings : get_bool ( " mcl_logging_mobs_spawn " , false )
2023-05-29 20:26:43 +02:00
local function mcl_log ( message , property )
2024-08-03 17:52:51 +02:00
if logging then
2023-05-29 20:26:43 +02:00
if property then
message = message .. " : " .. dump ( property )
end
2023-01-26 04:57:36 +01:00
mcl_util.mcl_log ( message , " [Mobs spawn] " , true )
end
end
2022-08-29 20:59:17 +02:00
local dbg_spawn_attempts = 0
local dbg_spawn_succ = 0
2022-10-07 22:46:13 +02:00
local dbg_spawn_counts = { }
2023-01-26 04:57:36 +01:00
2023-01-26 17:44:25 +01:00
local remove_far = true
2024-06-29 16:21:23 +02:00
local WAIT_FOR_SPAWN_ATTEMPT = 0.25
local FIND_SPAWN_POS_RETRIES = 1
2023-02-09 21:43:46 +01:00
local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8
2023-01-26 04:57:36 +01:00
2023-01-26 14:44:29 +01:00
local MOB_SPAWN_ZONE_INNER = 24
2024-07-31 02:30:29 +02:00
local MOB_SPAWN_ZONE_INNER_SQ = MOB_SPAWN_ZONE_INNER ^ 2 -- squared
2023-01-26 14:44:29 +01:00
local MOB_SPAWN_ZONE_MIDDLE = 32
2023-01-31 16:55:11 +01:00
local MOB_SPAWN_ZONE_OUTER = 128
2024-07-31 02:30:29 +02:00
local MOB_SPAWN_ZONE_OUTER_SQ = MOB_SPAWN_ZONE_OUTER ^ 2 -- squared
2023-01-26 14:44:29 +01:00
2021-04-08 18:07:20 +02:00
-- range for mob count
2023-01-26 14:44:29 +01:00
local MOB_CAP_INNER_RADIUS = 32
2022-07-17 05:42:45 +02:00
local aoc_range = 136
2023-01-26 17:44:25 +01:00
local MISSING_CAP_DEFAULT = 15
2023-05-29 20:26:43 +02:00
local MOBS_CAP_CLOSE = 10
2023-01-26 16:20:38 +01:00
2023-01-31 00:10:37 +01:00
local SPAWN_MAPGEN_LIMIT = mcl_vars.mapgen_limit - 150
2022-07-17 05:42:45 +02:00
local mob_cap = {
2023-01-29 00:48:48 +01:00
hostile = tonumber ( minetest.settings : get ( " mcl_mob_cap_monster " ) ) or 70 ,
2023-05-29 22:43:27 +02:00
passive = tonumber ( minetest.settings : get ( " mcl_mob_cap_animal " ) ) or 10 ,
2022-10-13 06:27:04 +02:00
ambient = tonumber ( minetest.settings : get ( " mcl_mob_cap_ambient " ) ) or 15 ,
2023-01-31 19:04:01 +01:00
water = tonumber ( minetest.settings : get ( " mcl_mob_cap_water " ) ) or 8 ,
2023-05-29 21:08:44 +02:00
water_ambient = tonumber ( minetest.settings : get ( " mcl_mob_cap_water_ambient " ) ) or 20 ,
2023-05-29 21:43:15 +02:00
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
2022-10-13 06:27:04 +02:00
player = tonumber ( minetest.settings : get ( " mcl_mob_cap_player " ) ) or 75 ,
2023-05-29 20:26:43 +02:00
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 ,
2022-10-13 06:27:04 +02:00
total = tonumber ( minetest.settings : get ( " mcl_mob_cap_total " ) ) or 500 ,
2022-07-17 05:42:45 +02:00
}
2021-04-08 12:04:36 +02:00
2023-05-29 22:43:27 +02:00
local peaceful_percentage_spawned = tonumber ( minetest.settings : get ( " mcl_mob_peaceful_percentage_spawned " ) ) or 30
2023-01-31 16:55:11 +01:00
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
2023-01-30 02:31:58 +01:00
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 )
2023-01-31 16:55:11 +01:00
mcl_log ( " Percentage of peaceful spawns are group: " .. peaceful_group_percentage_spawned )
mcl_log ( " Percentage of hostile spawns are group: " .. hostile_group_percentage_spawned )
2023-01-30 02:31:58 +01:00
2022-04-15 11:48:58 +02:00
--do mobs spawn?
local mobs_spawn = minetest.settings : get_bool ( " mobs_spawn " , true ) ~= false
2022-09-13 13:15:57 +02:00
local spawn_protected = minetest.settings : get_bool ( " mobs_spawn_protected " ) ~= false
2021-04-08 12:04:36 +02:00
2024-06-18 13:22:16 +02:00
local list_of_all_biomes = { }
2022-02-13 21:40:12 +01:00
2022-04-15 11:48:58 +02:00
-- count how many mobs are in an area
2022-07-17 05:42:45 +02:00
local function count_mobs ( pos , r , mob_type )
2021-04-08 08:07:15 +02:00
local num = 0
2022-07-17 05:42:45 +02:00
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
2021-04-08 08:07:15 +02:00
end
end
return num
end
2021-04-08 18:07:20 +02:00
2022-07-17 05:42:45 +02:00
local function count_mobs_total ( mob_type )
2022-07-17 03:45:41 +02:00
local num = 0
for _ , l in pairs ( minetest.luaentities ) do
if l.is_mob then
2022-07-17 05:42:45 +02:00
if mob_type == nil or l.type == mob_type then
num = num + 1
end
2022-07-17 03:45:41 +02:00
end
end
return num
end
2023-01-26 16:20:38 +01:00
local function count_mobs_add_entry ( mobs_list , mob_cat )
if mobs_list [ mob_cat ] then
mobs_list [ mob_cat ] = mobs_list [ mob_cat ] + 1
else
mobs_list [ mob_cat ] = 1
end
end
2023-01-29 00:48:48 +01:00
--categorise_by can be name or type or spawn_class
2023-01-26 16:20:38 +01:00
local function count_mobs_all ( categorise_by , pos )
local mobs_found_wide = { }
local mobs_found_close = { }
2023-01-07 21:37:29 +01:00
local num = 0
for _ , entity in pairs ( minetest.luaentities ) do
2023-01-26 04:57:36 +01:00
if entity and entity.is_mob then
local add_entry = false
2023-01-26 16:20:38 +01:00
--local mob_type = entity.type -- animal / monster / npc
local mob_cat = entity [ categorise_by ]
2023-01-26 04:57:36 +01:00
2023-01-26 16:20:38 +01:00
if pos then
2023-01-26 04:57:36 +01:00
local mob_pos = entity.object : get_pos ( )
if mob_pos then
local distance = vector.distance ( pos , mob_pos )
2023-02-09 21:43:46 +01:00
--mcl_log("distance: ".. distance)
2023-01-26 16:20:38 +01:00
if distance <= MOB_SPAWN_ZONE_MIDDLE then
2023-02-09 21:43:46 +01:00
--mcl_log("distance is close")
2023-01-26 16:20:38 +01:00
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
2023-02-09 21:43:46 +01:00
--mcl_log("distance is wide")
2023-01-26 16:20:38 +01:00
count_mobs_add_entry ( mobs_found_wide , mob_cat )
2023-01-26 04:57:36 +01:00
add_entry = true
else
2023-01-26 16:20:38 +01:00
--mcl_log("mob_pos: " .. minetest.pos_to_string(mob_pos))
2023-01-26 04:57:36 +01:00
end
end
2023-01-07 21:37:29 +01:00
else
2023-01-26 16:20:38 +01:00
count_mobs_add_entry ( mobs_found_wide , mob_cat )
2023-01-26 04:57:36 +01:00
add_entry = true
end
2023-01-26 16:20:38 +01:00
2023-01-26 04:57:36 +01:00
if add_entry then
num = num + 1
2023-01-07 21:37:29 +01:00
end
end
end
2023-02-09 21:43:46 +01:00
--mcl_log("num: ".. num)
2023-01-26 16:20:38 +01:00
return mobs_found_close , mobs_found_wide , num
2023-01-07 21:37:29 +01:00
end
2022-10-13 21:14:44 +02:00
local function count_mobs_total_cap ( mob_type )
2023-05-29 20:26:43 +02:00
local total = 0
2022-10-13 21:14:44 +02:00
local num = 0
2023-05-29 20:26:43 +02:00
local hostile = 0
local non_hostile = 0
2022-10-13 21:14:44 +02:00
for _ , l in pairs ( minetest.luaentities ) do
if l.is_mob then
2023-05-29 20:26:43 +02:00
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
2022-10-13 21:14:44 +02:00
num = num + 1
2023-05-29 20:26:43 +02:00
else
2023-05-29 21:08:44 +02:00
mcl_log ( " l.name " , l.name )
2023-05-29 20:26:43 +02:00
mcl_log ( " l.nametag " , l.nametag )
2022-10-13 21:14:44 +02:00
end
end
end
2023-05-29 20:26:43 +02:00
mcl_log ( " Total mobs " , total )
mcl_log ( " hostile " , hostile )
mcl_log ( " non_hostile " , non_hostile )
return num , non_hostile , hostile
2022-10-13 21:14:44 +02:00
end
2021-04-08 08:07:15 +02:00
2023-02-09 21:43:46 +01:00
local function output_mob_stats ( mob_counts , total_mobs , chat_display )
2023-01-26 04:57:36 +01:00
if ( total_mobs ) then
2023-02-09 21:43:46 +01:00
local total_output = " Total mobs found: " .. total_mobs
if chat_display then
minetest.log ( total_output )
else
minetest.log ( " action " , total_output )
end
2023-01-26 04:57:36 +01:00
end
2023-02-09 21:43:46 +01:00
local detailed = " "
2023-01-26 04:57:36 +01:00
if mob_counts then
for k , v1 in pairs ( mob_counts ) do
2023-02-09 21:43:46 +01:00
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 )
2023-01-26 04:57:36 +01:00
end
end
end
2021-04-08 08:07:15 +02:00
-- global functions
2022-05-25 14:44:49 +02:00
function mcl_mobs : spawn_abm_check ( pos , node , name )
2021-04-08 08:07:15 +02:00
-- global function to add additional spawn checks
-- return true to stop spawning mob
end
--[[
Custom elements changed :
2021-04-08 12:48:25 +02:00
2021-04-08 08:07:15 +02:00
name :
the mobs name
2021-04-12 15:45:00 +02:00
dimension :
2021-04-08 08:07:15 +02:00
" overworld "
" nether "
" end "
types of spawning :
" water "
" ground "
" lava "
2021-04-08 12:48:25 +02:00
biomes : tells the spawner to allow certain mobs to spawn in certain biomes
{ " this " , " that " , " grasslands " , " whatever " }
what is aoc ? ? ? objects in area
2021-04-08 08:07:15 +02:00
WARNING : BIOME INTEGRATION NEEDED -> How to get biome through lua ? ?
] ] --
2021-04-08 18:07:20 +02:00
2021-04-08 08:07:15 +02:00
--this is where all of the spawning information is kept
2021-04-08 14:09:43 +02:00
local spawn_dictionary = { }
2023-02-10 03:47:05 +01:00
--this is where all of the spawning information is kept for mobs that don't naturally spawn
local non_spawn_dictionary = { }
2021-04-08 08:07:15 +02:00
2022-05-25 14:44:49 +02:00
function mcl_mobs : spawn_setup ( def )
2022-04-15 11:48:58 +02:00
if not mobs_spawn then return end
2021-04-08 12:48:25 +02:00
2022-04-15 11:48:58 +02:00
if not def then
minetest.log ( " warning " , " Empty mob spawn setup definition " )
return
end
2021-04-08 12:48:25 +02:00
2022-04-15 11:48:58 +02:00
local name = def.name
if not name then
minetest.log ( " warning " , " Missing mob name " )
2021-04-08 08:07:15 +02:00
return
end
2022-04-15 11:48:58 +02:00
local dimension = def.dimension or " overworld "
local type_of_spawning = def.type_of_spawning or " ground "
local biomes = def.biomes or list_of_all_biomes
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
2021-04-08 08:07:15 +02:00
-- 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
2022-05-25 14:44:49 +02:00
minetest.log ( " warning " , string.format ( " [mcl_mobs] %s has spawning disabled " , name ) )
2021-04-08 08:07:15 +02:00
return
end
2022-05-25 14:44:49 +02:00
minetest.log ( " action " , string.format ( " [mcl_mobs] Chance setting for %s changed to %s (total: %s) " , name , chance , aoc ) )
2021-04-08 08:07:15 +02:00
end
2022-04-15 11:48:58 +02:00
if chance < 1 then
chance = 1
minetest.log ( " warning " , " Chance shouldn't be less than 1 (mob name: " .. name .. " ) " )
end
2021-04-08 08:07:15 +02:00
2022-04-15 11:48:58 +02:00
spawn_dictionary [ # spawn_dictionary + 1 ] = {
name = name ,
dimension = dimension ,
2022-07-17 05:42:45 +02:00
type_of_spawning = type_of_spawning ,
2022-04-15 11:48:58 +02:00
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
2021-04-08 08:07:15 +02:00
2023-02-10 03:47:05 +01:00
function mcl_mobs : mob_light_lvl ( mob_name , dimension )
2023-02-16 22:46:41 +01:00
local spawn_dictionary_consolidated = { }
2023-03-04 02:38:51 +01:00
if non_spawn_dictionary [ mob_name ] then
2023-02-16 22:46:41 +01:00
local mob_dimension = non_spawn_dictionary [ mob_name ] [ dimension ]
2023-03-04 02:38:51 +01:00
if mob_dimension then
--minetest.log("Found in non spawn dictionary for dimension")
return mob_dimension.min_light , mob_dimension.max_light
2023-02-10 03:47:05 +01:00
else
2023-03-04 02:38:51 +01:00
--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
2023-02-10 03:47:05 +01:00
end
else
2023-03-04 02:38:51 +01:00
--minetest.log("must be in spawning dictionary")
2023-02-10 03:47:05 +01:00
for i , v in pairs ( spawn_dictionary ) do
2023-03-04 02:38:51 +01:00
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
}
2023-02-16 22:46:41 +01:00
end
2023-03-04 02:38:51 +01:00
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")
2023-02-26 17:22:21 +01:00
return mob_dimension.min_light , mob_dimension.max_light
else
2023-03-04 02:38:51 +01:00
--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")
2023-02-26 17:22:21 +01:00
end
2023-02-10 03:47:05 +01:00
end
2023-03-04 02:38:51 +01:00
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
2023-02-10 03:47:05 +01:00
end
function mcl_mobs : non_spawn_specific ( mob_name , dimension , min_light , max_light )
table.insert ( non_spawn_dictionary , mob_name )
2023-03-16 20:32:43 +01:00
non_spawn_dictionary [ mob_name ] = {
[ dimension ] = {
2023-02-10 03:47:05 +01:00
min_light = min_light , max_light = max_light
}
}
end
2023-03-16 20:32:43 +01:00
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 )
2022-04-15 11:48:58 +02:00
-- Do mobs spawn at all?
2024-06-19 01:59:12 +02:00
if not mobs_spawn then return end
2021-04-08 08:07:15 +02:00
2024-04-27 14:29:00 +02:00
assert ( min_height )
assert ( max_height )
2022-04-15 11:48:58 +02:00
-- chance/spawn number override in minetest.conf for registered mob
local numbers = minetest.settings : get ( name )
2021-04-08 08:07:15 +02:00
2022-04-15 11:48:58 +02:00
if numbers then
numbers = numbers : split ( " , " )
chance = tonumber ( numbers [ 1 ] ) or chance
aoc = tonumber ( numbers [ 2 ] ) or aoc
2021-04-08 08:07:15 +02:00
2022-04-15 11:48:58 +02:00
if chance == 0 then
2022-05-25 14:44:49 +02:00
minetest.log ( " warning " , string.format ( " [mcl_mobs] %s has spawning disabled " , name ) )
2022-04-15 11:48:58 +02:00
return
end
2021-04-08 08:07:15 +02:00
2022-05-25 14:44:49 +02:00
minetest.log ( " action " , string.format ( " [mcl_mobs] Chance setting for %s changed to %s (total: %s) " , name , chance , aoc ) )
2021-04-08 18:07:20 +02:00
end
2021-04-08 08:07:15 +02:00
--load information into the spawn dictionary
2021-04-08 15:52:50 +02:00
local key = # spawn_dictionary + 1
2024-06-19 01:59:12 +02:00
spawn_dictionary [ key ] = {
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 ,
}
2024-04-14 11:52:54 +02:00
end
2022-04-15 11:48:58 +02:00
local function get_next_mob_spawn_pos ( pos )
2024-04-14 11:52:54 +02:00
-- Select a distance such that distances closer to the player are selected much more often than
2024-07-31 02:30:29 +02:00
-- 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
2024-04-14 11:52:54 +02:00
--print("Using spawn distance of "..tostring(distance).." fx="..tostring(fx)..",x="..tostring(x))
2023-02-09 21:43:46 +01:00
2024-07-31 02:30:29 +02:00
-- 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
2024-03-17 20:23:06 +01:00
local goal_pos = vector.offset ( pos , xoff , yoff , zoff )
2024-07-31 02:30:29 +02:00
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
2024-03-17 20:23:06 +01:00
mcl_log ( " Pos outside mapgen limits: " .. minetest.pos_to_string ( goal_pos ) )
return nil
end
2024-03-18 19:20:46 +01:00
-- Calculate upper/lower y limits
2024-07-31 02:30:29 +02:00
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
2024-03-18 19:20:46 +01:00
2024-07-31 02:30:29 +02:00
local y_min , y_max
if d2 >= MOB_SPAWN_ZONE_INNER_SQ then
2024-03-18 19:20:46 +01:00
-- Outer region, y range has both ends on the outer sphere
y_min = pos.y - y1
y_max = pos.y + y1
2024-03-17 21:54:39 +01:00
else
2024-03-18 19:20:46 +01:00
-- Inner region, y range spans between inner and outer spheres
2024-07-31 02:30:29 +02:00
local y2 = math_sqrt ( MOB_SPAWN_ZONE_INNER_SQ - d2 )
if goal_pos.y > pos.y then
2024-03-18 19:20:46 +01:00
-- 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
2024-03-17 21:54:39 +01:00
end
2024-05-06 09:10:57 +02:00
-- Limit total range of check to 32 nodes (maximum of 3 map blocks)
2024-07-31 02:30:29 +02:00
y_min = math_max ( math_floor ( y_min ) , goal_pos.y - 16 )
y_max = math_min ( math_ceil ( y_max ) , goal_pos.y + 16 )
2024-05-06 09:10:57 +02:00
2024-03-17 20:23:06 +01:00
-- Ask engine for valid spawn locations
local spawning_position_list = find_nodes_in_area_under_air (
2024-03-17 21:54:39 +01:00
{ x = goal_pos.x , y = y_min , z = goal_pos.z } ,
{ x = goal_pos.x , y = y_max , z = goal_pos.z } ,
2024-03-17 20:23:06 +01:00
{ " group:solid " , " group:water " , " group:lava " }
2024-04-14 11:52:54 +02:00
) or { }
2024-03-17 20:23:06 +01:00
-- 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
2021-04-08 08:07:15 +02:00
2024-03-17 20:23:06 +01:00
-- 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 ) ]
2021-04-08 08:07:15 +02:00
end
2021-04-08 15:52:50 +02:00
2022-04-29 01:28:00 +02:00
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
2021-04-08 08:07:15 +02:00
2022-07-05 03:49:46 +02:00
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
2022-10-13 21:13:21 +02:00
local function has_room ( self , pos )
local cb = 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 " )
local x = cb [ 4 ] - cb [ 1 ]
local y = cb [ 5 ] - cb [ 2 ]
local z = cb [ 6 ] - cb [ 3 ]
local r = math.ceil ( x * y * z )
local p1 = vector.offset ( pos , cb [ 1 ] , cb [ 2 ] , cb [ 3 ] )
local p2 = vector.offset ( pos , cb [ 4 ] , cb [ 5 ] , cb [ 6 ] )
local n = # minetest.find_nodes_in_area ( p1 , p2 , nodes ) or 0
if r > n then
2022-10-13 21:48:32 +02:00
minetest.log ( " warning " , " [mcl_mobs] No room for mob " .. self.name .. " at " .. minetest.pos_to_string ( vector.round ( pos ) ) )
2022-10-13 21:13:21 +02:00
return false
end
return true
end
2024-04-01 17:40:14 +02:00
mcl_mobs.custom_biomecheck = nil
2023-01-26 14:44:29 +01:00
2024-04-01 17:40:14 +02:00
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 )
else
local gotten_biome = minetest.get_biome_data ( pos )
if not gotten_biome then
return
end
gotten_biome = mt_get_biome_name ( gotten_biome.biome )
--minetest.log ("biome: " .. dump(gotten_biome))
return gotten_biome
end
end
2023-01-26 14:44:29 +01:00
2024-06-29 16:21:23 +02:00
local counts = { }
local function initial_spawn_check ( state , node , spawn_def )
local function log_fail ( reason )
local count = ( counts [ reason ] or 0 ) + 1
counts [ reason ] = count
2024-06-30 18:01:46 +02:00
if logging then
minetest.log ( " Spawn check for " .. tostring ( spawn_def and spawn_def.name ) .. " failed - " .. reason .. " ( " .. count .. " ) " .. dump ( {
state = state ,
node = node ,
} ) )
end
2024-06-29 16:21:23 +02:00
return false
2024-06-19 01:59:12 +02:00
end
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
if not spawn_def then return log_fail ( " missing spawn_def " ) end
local mob_def = minetest.registered_entities [ spawn_def.name ]
2024-06-19 01:59:12 +02:00
2024-06-29 16:21:23 +02:00
if mob_def.type == " monster " then
if not state.spawn_hostile then return log_fail ( " can't spawn hostile " ) end
else
if not state.spawn_passive then return log_fail ( " can't spawn passive " ) end
2024-06-19 01:59:12 +02:00
end
2024-06-29 16:21:23 +02:00
-- Make the dimention is correct
if spawn_def.dimension ~= state.dimension then return log_fail ( " incorrect dimension " ) end
if type ( spawn_def.biomes ) ~= " table " or not table.find ( spawn_def.biomes , state.biome ) then
return log_fail ( " Incorrect biome " )
2024-06-19 01:59:12 +02:00
end
2024-06-29 16:21:23 +02:00
-- Ground mobs must spawn on solid nodes that are not leafes
if spawn_def.type_of_spawning == " ground " and not state.is_ground then
return log_fail ( " not ground node " )
end
-- Water mobs must spawn in water
if spawn_def.type_of_spawning == " water " and not state.is_water then return log_fail ( " not water node " ) end
-- Farm animals must spawn on grass
if is_farm_animal ( spawn_def.name ) and not state.is_grass then return log_fail ( " not grass block " ) end
2024-06-19 01:59:12 +02:00
2024-06-29 16:21:23 +02:00
return true
2024-06-19 01:59:12 +02:00
end
2024-06-29 16:21:23 +02:00
local function spawn_check ( pos , state , node , spawn_def )
2024-06-18 01:21:38 +02:00
local function log_fail ( reason )
local count = ( counts [ reason ] or 0 ) + 1
counts [ reason ] = count
mcl_log ( " Spawn check failed - " .. reason .. " ( " .. count .. " ) " )
return false
end
2024-06-29 16:21:23 +02:00
if not initial_spawn_check ( state , node , spawn_def ) then return false end
2023-03-16 20:32:43 +01:00
2022-08-29 20:59:17 +02:00
dbg_spawn_attempts = dbg_spawn_attempts + 1
2024-06-18 01:21:38 +02:00
-- Make sure the mob can spawn at this location
if pos.y < spawn_def.min_height or pos.y > spawn_def.max_height then return log_fail ( " incorrect height " ) end
2024-06-29 16:21:23 +02:00
-- Spawns require enough room for the mob
local mob_def = minetest.registered_entities [ spawn_def.name ]
if not has_room ( mob_def , pos ) then return log_fail ( " mob doesn't fit here " ) end
2023-03-16 21:22:52 +01:00
2024-06-29 16:21:23 +02:00
-- Don't spawn if the spawn definition has a custom check and that fails
if spawn_def.check_position and not spawn_def.check_position ( pos ) then return log_fail ( " custom position check failed " ) end
return true
end
function mcl_mobs . spawn ( pos , id )
local def = minetest.registered_entities [ id ] or minetest.registered_entities [ " mobs_mc: " .. id ] or minetest.registered_entities [ " extra_mobs: " .. id ]
if not def or ( def.can_spawn and not def.can_spawn ( pos ) ) or not def.is_mob then
return false
end
if not dbg_spawn_counts [ def.name ] then
dbg_spawn_counts [ def.name ] = 1
else
dbg_spawn_counts [ def.name ] = dbg_spawn_counts [ def.name ] + 1
end
return minetest.add_entity ( pos , def.name )
end
local function build_state_for_position ( pos , parent_state )
-- Get spawning parameters for this location
2024-04-01 17:40:14 +02:00
local biome_name = get_biome_name ( pos )
if not biome_name then return end
2022-08-29 20:59:17 +02:00
2024-06-29 16:21:23 +02:00
local dimension = mcl_worlds.pos_to_dimension ( pos )
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
-- Get node and make sure it's loaded and a valid spawn point
local node = get_node ( pos )
2024-06-30 17:12:12 +02:00
local node_name = node.name
2024-06-18 01:21:38 +02:00
2024-06-30 18:01:46 +02:00
-- Make sure we can spawn here
if not node or node_name == " ignore " or node_name == " mcl_core:bedrock " then return end
2024-06-29 16:21:23 +02:00
-- Check if it's ground
2024-06-30 17:12:12 +02:00
local is_water = get_item_group ( node_name , " water " ) ~= 0
local is_ground = false
if not is_water then
is_ground = get_item_group ( node_name , " solid " ) ~= 0
if not is_ground then
pos.y = pos.y - 1
node = get_node ( pos )
2024-06-30 18:01:46 +02:00
node_name = node.name
2024-06-30 17:12:12 +02:00
is_ground = get_item_group ( node_name , " solid " ) ~= 0
end
pos.y = pos.y + 1
2022-08-29 20:59:17 +02:00
end
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
-- Build spawn state data
local state = {
spawn_hostile = true ,
spawn_passive = true ,
}
if parent_state then state = table.copy ( parent_state ) end
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
state.biome = biome_name
state.dimension = dimension
2024-06-18 01:21:38 +02:00
2024-06-30 17:12:12 +02:00
state.is_ground = is_ground and get_item_group ( node_name , " leaves " ) == 0
2024-06-30 18:01:46 +02:00
state.is_grass = get_item_group ( node_name , " grass_block " ) ~= 0
state.is_water = is_water
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
-- Check light level
2024-06-18 01:21:38 +02:00
local gotten_light = get_node_light ( pos )
-- Legacy lighting
if not modern_lighting then
if gotten_light < spawn_def.min_light or gotten_light > spawn_def.max_light then
2024-06-29 16:21:23 +02:00
state.light = gotten_light
2024-06-18 01:21:38 +02:00
end
2024-06-29 16:21:23 +02:00
else
-- Modern lighting
2024-06-30 17:12:12 +02:00
local light_node = get_node ( pos )
2024-06-29 16:21:23 +02:00
local sky_light = minetest.get_natural_light ( pos ) or 0
local art_light = minetest.get_artificial_light ( light_node.param1 )
2024-06-18 01:21:38 +02:00
if dimension == " nether " then
if art_light > nether_threshold then
2024-06-29 16:21:23 +02:00
state.spawn_hostile = false
2023-03-16 20:32:43 +01:00
end
2024-06-18 01:21:38 +02:00
elseif dimension == " end " then
if art_light > end_threshold then
2024-06-29 16:21:23 +02:00
state.spawn_hostile = false
2024-06-18 01:21:38 +02:00
end
elseif dimension == " overworld " then
if art_light > overworld_threshold then
2024-06-29 16:21:23 +02:00
state.spawn_hostile = false
2024-06-18 01:21:38 +02:00
end
if sky_light > overworld_sky_threshold then
2024-06-29 16:21:23 +02:00
state.spawn_hostile = false
2024-06-18 01:21:38 +02:00
end
end
2024-06-29 16:21:23 +02:00
2024-06-18 01:21:38 +02:00
-- passive threshold is apparently the same in all dimensions ...
if gotten_light < overworld_passive_threshold then
2024-06-29 16:21:23 +02:00
state.spawn_passive = false
2022-08-29 20:59:17 +02:00
end
end
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
return state , node
2022-08-29 20:59:17 +02:00
end
2022-07-17 03:45:41 +02:00
2024-06-29 16:21:23 +02:00
local function spawn_group ( p , mob , spawn_on , amount_to_spawn , parent_state )
2024-06-30 17:12:12 +02:00
local nn = find_nodes_in_area_under_air ( vector.offset ( p , - 5 , - 3 , - 5 ) , vector.offset ( p , 5 , 3 , 5 ) , spawn_on )
2022-07-17 02:20:12 +02:00
local o
2022-08-29 20:59:17 +02:00
table.shuffle ( nn )
2022-07-10 12:16:19 +02:00
if not nn or # nn < 1 then
nn = { }
table.insert ( nn , p )
end
2023-01-26 04:57:36 +01:00
for i = 1 , amount_to_spawn do
2022-07-17 05:42:45 +02:00
local sp = vector.offset ( nn [ math.random ( # nn ) ] , 0 , 1 , 0 )
2024-06-29 16:21:23 +02:00
local state , node = build_state_for_position ( sp , parent_state )
if spawn_check ( sp , state , node , mob ) then
2022-08-29 20:59:17 +02:00
if mob.type_of_spawning == " water " then
sp = get_water_spawn ( sp )
end
2022-09-09 22:41:04 +02:00
o = mcl_mobs.spawn ( sp , mob.name )
2022-08-29 20:59:17 +02:00
if o then dbg_spawn_succ = dbg_spawn_succ + 1 end
2022-07-17 05:42:45 +02:00
end
2022-07-05 12:55:14 +02:00
end
2022-07-17 02:20:12 +02:00
return o
2022-07-05 12:55:14 +02:00
end
2022-09-09 23:31:02 +02:00
mcl_mobs.spawn_group = spawn_group
2022-08-01 05:35:47 +02:00
2022-10-12 01:21:15 +02:00
local S = minetest.get_translator ( " mcl_mobs " )
2022-08-01 05:35:47 +02:00
minetest.register_chatcommand ( " spawn_mob " , {
privs = { debug = true } ,
2022-12-22 03:13:26 +01:00
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<mobs_variable=variable_value>:'. 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<passive=true>: \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<jump_height=NUM10>: \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<passive=true>::var<jump_height=NUM10>::var<fly_in=air>::var<fly=true>: \n etc. " ) ,
2022-08-01 05:35:47 +02:00
func = function ( n , param )
local pos = minetest.get_player_by_name ( n ) : get_pos ( )
2022-10-12 01:21:15 +02:00
local modifiers = { }
for capture in string.gmatch ( param , " %:(.-)%: " ) do
table.insert ( modifiers , " : " .. capture )
end
local mobname = param
2024-06-30 17:12:12 +02:00
local mod1 = string.find ( param , " : " )
2022-10-12 01:21:15 +02:00
if mod1 then
mobname = string.sub ( param , 1 , mod1 - 1 )
end
local mob = mcl_mobs.spawn ( pos , mobname )
2022-12-22 03:13:26 +01:00
if mob then
for c = 1 , # modifiers do
modifs = modifiers [ c ]
2022-10-12 01:21:15 +02:00
2022-12-22 03:13:26 +01:00
local mod1 = string.find ( modifs , " : " )
local mod_start = string.find ( modifs , " < " )
local mod_vals = string.find ( modifs , " = " )
local mod_end = string.find ( modifs , " > " )
2022-10-12 01:21:15 +02:00
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 ) )
2022-12-22 03:13:26 +01:00
return true , mobname .. " spawned at " .. minetest.pos_to_string ( pos )
else
return false , " Couldn't spawn " .. mobname
2022-08-01 05:35:47 +02:00
end
end
} )
2021-04-10 23:54:40 +02:00
if mobs_spawn then
2023-05-29 20:26:43 +02:00
local function mob_cap_space ( pos , mob_type , mob_counts_close , mob_counts_wide , cap_space_hostile , cap_space_non_hostile )
2023-01-31 16:55:11 +01:00
-- Some mob examples
--type = "monster", spawn_class = "hostile",
--type = "animal", spawn_class = "passive",
--local cod = { type = "animal", spawn_class = "water",
2023-01-26 16:20:38 +01:00
2023-01-26 17:44:25 +01:00
local type_cap = mob_cap [ mob_type ] or MISSING_CAP_DEFAULT
2023-01-26 16:20:38 +01:00
local close_zone_cap = MOBS_CAP_CLOSE
local mob_total_wide = mob_counts_wide [ mob_type ]
if not mob_total_wide then
2023-01-27 03:47:01 +01:00
--mcl_log("none of type found. set as 0")
2023-01-26 16:20:38 +01:00
mob_total_wide = 0
end
2024-07-31 02:30:29 +02:00
local cap_space_wide = math_max ( type_cap - mob_total_wide , 0 )
2023-05-29 20:26:43 +02:00
2023-05-29 21:08:44 +02:00
mcl_log ( " mob_type " , mob_type )
mcl_log ( " cap_space_wide " , cap_space_wide )
2023-05-29 20:26:43 +02:00
local cap_space_available = 0
if mob_type == " hostile " then
mcl_log ( " cap_space_global " , cap_space_hostile )
2024-07-31 02:30:29 +02:00
cap_space_available = math_min ( cap_space_hostile , cap_space_wide )
2023-05-29 20:26:43 +02:00
else
mcl_log ( " cap_space_global " , cap_space_non_hostile )
2024-07-31 02:30:29 +02:00
cap_space_available = math_min ( cap_space_non_hostile , cap_space_wide )
2023-01-26 16:20:38 +01:00
end
local mob_total_close = mob_counts_close [ mob_type ]
if not mob_total_close then
2023-01-27 03:47:01 +01:00
--mcl_log("none of type found. set as 0")
2023-01-26 16:20:38 +01:00
mob_total_close = 0
end
2024-07-31 02:30:29 +02:00
local cap_space_close = math_max ( close_zone_cap - mob_total_close , 0 )
cap_space_available = math_min ( cap_space_available , cap_space_close )
2023-01-26 16:20:38 +01:00
2023-05-29 21:08:44 +02:00
mcl_log ( " cap_space_close " , cap_space_close )
mcl_log ( " cap_space_available " , cap_space_available )
2023-01-31 16:55:11 +01:00
2023-01-31 00:10:37 +01:00
if false and mob_type == " water " then
2023-01-29 00:48:48 +01:00
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
2023-01-28 03:31:41 +01:00
2023-05-29 20:26:43 +02:00
return cap_space_available
2023-01-26 04:57:36 +01:00
end
2023-01-07 21:37:29 +01:00
2023-01-28 01:14:08 +01:00
local function find_spawning_position ( pos , max_times )
local spawning_position
2024-03-17 20:23:06 +01:00
local max_loops = max_times or 1
2023-01-31 00:10:37 +01:00
2023-05-29 20:26:43 +02:00
--mcl_log("mapgen_limit: " .. SPAWN_MAPGEN_LIMIT)
2024-03-17 20:23:06 +01:00
while max_loops > 0 do
local spawning_position = get_next_mob_spawn_pos ( pos )
2024-03-17 21:33:35 +01:00
if spawning_position then return spawning_position end
2024-03-17 20:23:06 +01:00
max_loops = max_loops - 1
end
2024-06-30 17:12:12 +02:00
2024-03-17 20:23:06 +01:00
return nil
2023-01-27 04:50:00 +01:00
end
2022-04-15 11:48:58 +02:00
2024-06-29 16:21:23 +02:00
local function select_random_mob_def ( spawn_table )
if # spawn_table == 0 then return nil end
2024-06-18 01:21:38 +02:00
2024-06-29 16:21:23 +02:00
local cumulative_chance = 0
for i = 1 , # spawn_table do
cumulative_chance = cumulative_chance + spawn_table [ i ] . chance
2023-11-23 00:11:13 +01:00
end
2024-06-18 01:21:38 +02:00
local mob_chance_offset = math_random ( 1 , 1e6 ) / 1e6 * cumulative_chance
2024-06-30 18:01:46 +02:00
--minetest.log("action", "mob_chance_offset = "..tostring(mob_chance_offset).."/"..tostring(cumulative_chance))
2023-01-27 04:50:00 +01:00
2024-06-29 16:21:23 +02:00
for i = 1 , # spawn_table do
local mob_def = spawn_table [ i ]
2024-06-18 01:21:38 +02:00
local mob_chance = mob_def.chance
if mob_chance_offset <= mob_chance then
2024-06-30 18:01:46 +02:00
--minetest.log(mob_def.name.." "..mob_chance)
2024-06-18 01:21:38 +02:00
return mob_def
end
mob_chance_offset = mob_chance_offset - mob_chance
end
assert ( not " failed " )
end
2024-06-29 16:21:23 +02:00
local spawn_lists = { }
local function get_spawn_list ( pos , cap_space_hostile , cap_space_non_hostile )
local spawn_hostile = false
local spawn_passive = false
-- Check capacity
local mob_counts_close , mob_counts_wide , total_mobs = count_mobs_all ( " spawn_class " , pos )
local cap_space_hostile = mob_cap_space ( pos , " hostile " , mob_counts_close , mob_counts_wide , cap_space_hostile , cap_space_non_hostile )
if cap_space_hostile > 0 then
spawn_hostile = true
end
local cap_space_passive = mob_cap_space ( pos , " passive " , mob_counts_close , mob_counts_wide , cap_space_hostile , cap_space_non_hostile )
if cap_space_passive > 0 then
if math.random ( 100 ) < peaceful_percentage_spawned then
spawn_passive = true
end
end
-- Merge light level chcekss with cap checks
local state , node = build_state_for_position ( pos )
if not state then return end
state.spawn_hostile = spawn_hostile and state.spawn_hostile
state.spawn_passive = spawn_passive and state.spawn_passive
-- Make sure it is possible to spawn a mob here
if not state.spawn_hostile and not state.spawn_passive then return end
-- Check the cache to see if we have already built a spawn list for this state
local state_hash = compute_hash ( state ) -- from mcl_enchanting
local spawn_list = spawn_lists [ state_hash ]
state.cap_space_hostile = cap_space_hostile
state.cap_space_passive = cap_space_passive
if spawn_list then
return spawn_list , state , node
end
-- Build a spawn list for this state
spawn_list = { }
local spawn_names = { }
for _ , def in pairs ( spawn_dictionary ) do
if initial_spawn_check ( state , node , def ) then
table.insert ( spawn_list , def )
table.insert ( spawn_names , def.name )
end
end
if logging then
minetest.log ( dump ( {
pos = pos ,
node = node ,
state = state ,
state_hash = state_hash ,
spawn_names = spawn_names ,
} ) )
end
spawn_lists [ state_hash ] = spawn_list
return spawn_list , state , node
end
2024-06-18 01:21:38 +02:00
-- Spawns one mob or one group of mobs
2024-06-29 16:21:23 +02:00
local fail_count = 0
2024-06-18 01:21:38 +02:00
local function spawn_a_mob ( pos , cap_space_hostile , cap_space_non_hostile )
2023-01-28 01:14:08 +01:00
local spawning_position = find_spawning_position ( pos , FIND_SPAWN_POS_RETRIES )
2023-01-27 04:50:00 +01:00
if not spawning_position then
2024-06-29 16:21:23 +02:00
fail_count = fail_count + 1
if fail_count > 16 then
minetest.log ( " action " , " [Mobs spawn] Cannot find a valid spawn position after retries: " .. FIND_SPAWN_POS_RETRIES )
end
2023-01-27 04:50:00 +01:00
return
end
2024-06-29 16:21:23 +02:00
fail_count = 0
-- Spawning prohibited in protected areas
if spawn_protected and minetest.is_protected ( spawning_position , " " ) then return end
-- Select a mob
local spawn_list , state , node = get_spawn_list ( spawning_position , cap_space_hostile , cap_space_non_hostile )
if not spawn_list then return end
local mob_def = select_random_mob_def ( spawn_list )
if not mob_def or not mob_def.name then return end
local mob_def_ent = minetest.registered_entities [ mob_def.name ]
if not mob_def_ent then return end
local cap_space_available = state.cap_space_passive
if mob_def_ent.type == " monster " then
cap_space_available = state.cap_space_hostile
end
2023-01-26 04:57:36 +01:00
2024-06-29 16:21:23 +02:00
-- Make sure we would be spawning a mob
if not spawn_check ( spawning_position , state , node , mob_def ) then
mcl_log ( " Spawn check failed " )
return
end
2023-01-29 00:48:48 +01:00
2024-06-29 16:21:23 +02:00
-- Water mob special case
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 ) ) )
2024-06-18 01:21:38 +02:00
return
end
2024-06-29 16:21:23 +02:00
end
2023-01-29 00:48:48 +01:00
2024-06-29 16:21:23 +02:00
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
2023-05-29 20:26:43 +02:00
2024-06-29 16:21:23 +02:00
--everything is correct, spawn mob
local spawn_in_group = mob_def_ent.spawn_in_group or 4
2023-01-30 02:31:58 +01:00
2024-06-29 16:21:23 +02:00
local spawned
if spawn_in_group then
local group_min = mob_def_ent.spawn_in_group_min or 1
if not group_min then group_min = 1 end
2023-01-26 04:57:36 +01:00
2024-06-29 16:21:23 +02:00
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 )
2023-01-27 04:50:00 +01:00
2024-06-29 16:21:23 +02:00
if amount_to_spawn > 1 then
2024-06-18 01:21:38 +02:00
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 )
)
2022-10-13 06:26:51 +02:00
end
2024-06-29 16:21:23 +02:00
return spawn_group ( spawning_position , mob_def , { minetest.get_node ( vector.offset ( spawning_position , 0 , - 1 , 0 ) ) . name } , amount_to_spawn , state )
2022-04-15 11:48:58 +02:00
end
2024-06-18 01:21:38 +02:00
end
2024-06-29 16:21:23 +02:00
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 )
)
2022-04-15 11:48:58 +02:00
end
2024-06-29 16:21:23 +02:00
return mcl_mobs.spawn ( spawning_position , mob_def.name )
2022-04-15 11:48:58 +02:00
end
2021-04-08 08:07:15 +02:00
2022-04-15 11:48:58 +02:00
--MAIN LOOP
local timer = 0
minetest.register_globalstep ( function ( dtime )
timer = timer + dtime
2023-01-26 04:57:36 +01:00
if timer < WAIT_FOR_SPAWN_ATTEMPT then return end
2022-04-15 11:48:58 +02:00
timer = 0
2023-01-27 04:50:00 +01:00
2024-06-18 01:21:38 +02:00
local start_time_us = minetest.get_us_time ( )
2022-10-13 06:27:04 +02:00
local players = get_connected_players ( )
2023-05-29 20:26:43 +02:00
local total_mobs , total_non_hostile , total_hostile = count_mobs_total_cap ( )
2024-07-31 02:30:29 +02:00
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 )
2023-05-29 20:26:43 +02:00
mcl_log ( " global cap_space_hostile " , cap_space_hostile )
mcl_log ( " global cap_space_non_hostile " , cap_space_non_hostile )
2022-10-13 06:27:04 +02:00
if total_mobs > mob_cap.total or total_mobs > # players * mob_cap.player then
2022-10-13 21:14:44 +02:00
minetest.log ( " action " , " [mcl_mobs] global mob cap reached. no cycle spawning. " )
2024-06-29 16:21:23 +02:00
local took = ( minetest.get_us_time ( ) - start_time_us )
2024-06-30 17:12:12 +02:00
if took > debug_time_threshold then
2024-06-29 16:21:23 +02:00
minetest.log ( " action " , " [mcl_mobs] took " .. took .. " us " )
end
2022-10-13 06:27:04 +02:00
return
end --mob cap per player
2023-01-27 04:50:00 +01:00
2022-10-13 06:27:04 +02:00
for _ , player in pairs ( players ) do
2022-04-15 11:48:58 +02:00
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
2023-05-29 20:26:43 +02:00
spawn_a_mob ( pos , cap_space_hostile , cap_space_non_hostile )
2021-04-10 23:54:40 +02:00
end
end
2024-06-29 16:21:23 +02:00
local took = ( minetest.get_us_time ( ) - start_time_us )
2024-06-30 17:12:12 +02:00
if took > debug_time_threshold then
2024-06-29 16:21:23 +02:00
minetest.log ( " action " , " [mcl_mobs] took " .. took .. " us " )
end
2021-04-10 23:54:40 +02:00
end )
2021-04-08 12:48:25 +02:00
end
2022-08-29 20:59:17 +02:00
2023-05-04 22:14:16 +02:00
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 " )
2023-01-07 00:39:56 +01:00
function mob_class : check_despawn ( pos , dtime )
self.lifetimer = self.lifetimer - dtime
2022-11-11 00:21:47 +01:00
-- Despawning: when lifetimer expires, remove mob
2023-05-04 22:14:16 +02:00
if remove_far and despawn_allowed ( self ) then
2022-11-11 00:21:47 +01:00
if self.despawn_immediately or self.lifetimer <= 0 then
2022-11-11 17:28:45 +01:00
if logging then
2022-11-11 00:21:47 +01:00
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
2022-08-29 20:59:17 +02:00
minetest.register_chatcommand ( " mobstats " , {
privs = { debug = true } ,
func = function ( n , param )
2023-02-09 21:43:46 +01:00
--minetest.chat_send_player(n,dump(dbg_spawn_counts))
2022-08-29 20:59:17 +02:00
local pos = minetest.get_player_by_name ( n ) : get_pos ( )
2023-02-09 21:43:46 +01:00
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 )
2023-01-07 21:37:29 +01:00
2023-01-26 16:25:59 +01:00
local mob_counts_close , mob_counts_wide , total_mobs = count_mobs_all ( " name " ) -- Can use "type"
2023-02-09 21:43:46 +01:00
output_mob_stats ( mob_counts_wide , total_mobs , true )
2022-08-29 20:59:17 +02:00
end
} )
2024-06-18 13:22:16 +02:00
minetest.register_on_mods_loaded ( function ( )
for _ , def in pairs ( minetest.registered_biomes ) do
table.insert ( list_of_all_biomes , def.name )
end
end )