mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-11-22 10:31:06 +01:00
Choose direction vectors uniformly for spawning (#4467)
The previous code was biased towards placing mobs on top or below the player, because it chose the theta inclination angle uniformly, but the sphere is more narrow at the top and bottom. This code is also simpler. Reviewed-on: https://git.minetest.land/VoxeLibre/VoxeLibre/pulls/4467 Reviewed-by: teknomunk <teknomunk@protonmail.com> Co-authored-by: kno10 <erich.schubert@gmail.com> Co-committed-by: kno10 <erich.schubert@gmail.com>
This commit is contained in:
parent
32148262e1
commit
80a6a6efb0
1 changed files with 31 additions and 66 deletions
|
@ -17,12 +17,13 @@ local mt_get_biome_name = minetest.get_biome_name
|
||||||
local get_objects_inside_radius = minetest.get_objects_inside_radius
|
local get_objects_inside_radius = minetest.get_objects_inside_radius
|
||||||
local get_connected_players = minetest.get_connected_players
|
local get_connected_players = minetest.get_connected_players
|
||||||
|
|
||||||
|
local math_min = math.min
|
||||||
|
local math_max = math.max
|
||||||
local math_random = math.random
|
local math_random = math.random
|
||||||
local math_floor = math.floor
|
local math_floor = math.floor
|
||||||
local math_ceil = math.ceil
|
local math_ceil = math.ceil
|
||||||
local math_cos = math.cos
|
local math_cos = math.cos
|
||||||
local math_sin = math.sin
|
local math_sin = math.sin
|
||||||
local math_round = function(x) return (x > 0) and math_floor(x + 0.5) or math_ceil(x - 0.5) end
|
|
||||||
local math_sqrt = math.sqrt
|
local math_sqrt = math.sqrt
|
||||||
|
|
||||||
local vector_distance = vector.distance
|
local vector_distance = vector.distance
|
||||||
|
@ -54,8 +55,10 @@ local FIND_SPAWN_POS_RETRIES = 16
|
||||||
local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8
|
local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8
|
||||||
|
|
||||||
local MOB_SPAWN_ZONE_INNER = 24
|
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_MIDDLE = 32
|
||||||
local MOB_SPAWN_ZONE_OUTER = 128
|
local MOB_SPAWN_ZONE_OUTER = 128
|
||||||
|
local MOB_SPAWN_ZONE_OUTER_SQ = MOB_SPAWN_ZONE_OUTER^2 -- squared
|
||||||
|
|
||||||
-- range for mob count
|
-- range for mob count
|
||||||
local MOB_CAP_INNER_RADIUS = 32
|
local MOB_CAP_INNER_RADIUS = 32
|
||||||
|
@ -601,71 +604,40 @@ function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_
|
||||||
spawn_dictionary[key]["check_position"] = check_position
|
spawn_dictionary[key]["check_position"] = check_position
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate the inverse of a piecewise linear function f(x). Line segments are represented as two
|
|
||||||
-- adjacent points specified as { x, f(x) }. At least 2 points are required. If there are most solutions,
|
|
||||||
-- the one with a lower x value will be chosen.
|
|
||||||
local function inverse_pwl(fx, f)
|
|
||||||
if fx < f[1][2] then
|
|
||||||
return f[1][1]
|
|
||||||
end
|
|
||||||
|
|
||||||
for i=2,#f do
|
|
||||||
local x0,fx0 = unpack(f[i-1])
|
|
||||||
local x1,fx1 = unpack(f[i ])
|
|
||||||
if fx < fx1 then
|
|
||||||
return (fx - fx0) * (x1 - x0) / (fx1 - fx0) + x0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return f[#f][1]
|
|
||||||
end
|
|
||||||
|
|
||||||
local SPAWN_DISTANCE_CDF_PWL = {
|
|
||||||
{0.000,0.00},
|
|
||||||
{0.083,0.40},
|
|
||||||
{0.416,0.75},
|
|
||||||
{1.000,1.00},
|
|
||||||
}
|
|
||||||
|
|
||||||
local two_pi = 2 * math.pi
|
|
||||||
local function get_next_mob_spawn_pos(pos)
|
local function get_next_mob_spawn_pos(pos)
|
||||||
-- Select a distance such that distances closer to the player are selected much more often than
|
-- Select a distance such that distances closer to the player are selected much more often than
|
||||||
-- those further away from the player.
|
-- those further away from the player. This does produce a concentration at INNER (24 blocks)
|
||||||
local fx = (math_random(1,10000)-1) / 10000
|
local distance = math_random()^2 * (MOB_SPAWN_ZONE_OUTER - MOB_SPAWN_ZONE_INNER) + MOB_SPAWN_ZONE_INNER
|
||||||
local x = inverse_pwl(fx, SPAWN_DISTANCE_CDF_PWL)
|
|
||||||
local distance = x * (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))
|
--print("Using spawn distance of "..tostring(distance).." fx="..tostring(fx)..",x="..tostring(x))
|
||||||
|
|
||||||
-- TODO Floor xoff and zoff and add 0.5 so it tries to spawn in the middle of the square. Less failed attempts.
|
-- Choose a random direction. Rejection sampling is simple and fast (1-2 tries usually)
|
||||||
-- Use spherical coordinates https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
|
local xoff, yoff, zoff, dd
|
||||||
local theta = math_random() * two_pi
|
repeat
|
||||||
local phi = math_random() * two_pi
|
xoff, yoff, zoff = math_random() * 2 - 1, math_random() * 2 - 1, math_random() * 2 - 1
|
||||||
local xoff = math_round(distance * math_sin(theta) * math_cos(phi))
|
dd = xoff*xoff + yoff*yoff + zoff*zoff
|
||||||
local yoff = math_round(distance * math_cos(theta))
|
until (dd <= 1 and dd >= 1e-6) -- outside of uniform ball, retry
|
||||||
local zoff = math_round(distance * math_sin(theta) * math_sin(phi))
|
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)
|
local goal_pos = vector.offset(pos, xoff, yoff, zoff)
|
||||||
|
|
||||||
if not ( math.abs(goal_pos.x) <= SPAWN_MAPGEN_LIMIT and math.abs(pos.y) <= SPAWN_MAPGEN_LIMIT and math.abs(goal_pos.z) <= SPAWN_MAPGEN_LIMIT ) then
|
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))
|
mcl_log("Pos outside mapgen limits: " .. minetest.pos_to_string(goal_pos))
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate upper/lower y limits
|
-- Calculate upper/lower y limits
|
||||||
local R1 = MOB_SPAWN_ZONE_OUTER
|
local d2 = xoff*xoff + zoff*zoff -- squared distance in x,z plane only
|
||||||
local d = vector_distance( pos, vector.new( goal_pos.x, pos.y, goal_pos.z ) ) -- distance from player to projected point on horizontal plane
|
local y1 = math_sqrt( MOB_SPAWN_ZONE_OUTER_SQ - d2 ) -- absolue value of distance to outer sphere
|
||||||
local y1 = math_sqrt( R1*R1 - d*d ) -- absolue value of distance to outer sphere
|
|
||||||
|
|
||||||
local y_min
|
local y_min, y_max
|
||||||
local y_max
|
if d2 >= MOB_SPAWN_ZONE_INNER_SQ then
|
||||||
if d >= MOB_SPAWN_ZONE_INNER then
|
|
||||||
-- Outer region, y range has both ends on the outer sphere
|
-- Outer region, y range has both ends on the outer sphere
|
||||||
y_min = pos.y - y1
|
y_min = pos.y - y1
|
||||||
y_max = pos.y + y1
|
y_max = pos.y + y1
|
||||||
else
|
else
|
||||||
-- Inner region, y range spans between inner and outer spheres
|
-- Inner region, y range spans between inner and outer spheres
|
||||||
local R2 = MOB_SPAWN_ZONE_INNER
|
local y2 = math_sqrt( MOB_SPAWN_ZONE_INNER_SQ - d2 )
|
||||||
local y2 = math_sqrt( R2*R2 - d*d )
|
if goal_pos.y > pos.y then
|
||||||
if goal_pos.y > pos. y then
|
|
||||||
-- Upper hemisphere
|
-- Upper hemisphere
|
||||||
y_min = pos.y + y2
|
y_min = pos.y + y2
|
||||||
y_max = pos.y + y1
|
y_max = pos.y + y1
|
||||||
|
@ -675,16 +647,9 @@ local function get_next_mob_spawn_pos(pos)
|
||||||
y_max = pos.y - y2
|
y_max = pos.y - y2
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
y_min = math_round(y_min)
|
|
||||||
y_max = math_round(y_max)
|
|
||||||
|
|
||||||
-- Limit total range of check to 32 nodes (maximum of 3 map blocks)
|
-- Limit total range of check to 32 nodes (maximum of 3 map blocks)
|
||||||
if y_max > goal_pos.y + 16 then
|
y_min = math_max(math_floor(y_min), goal_pos.y - 16)
|
||||||
y_max = goal_pos.y + 16
|
y_max = math_min(math_ceil(y_max), goal_pos.y + 16)
|
||||||
end
|
|
||||||
if y_min < goal_pos.y - 16 then
|
|
||||||
y_min = goal_pos.y - 16
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ask engine for valid spawn locations
|
-- Ask engine for valid spawn locations
|
||||||
local spawning_position_list = find_nodes_in_area_under_air(
|
local spawning_position_list = find_nodes_in_area_under_air(
|
||||||
|
@ -997,7 +962,7 @@ if mobs_spawn then
|
||||||
mob_total_wide = 0
|
mob_total_wide = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local cap_space_wide = math.max(type_cap - mob_total_wide, 0)
|
local cap_space_wide = math_max(type_cap - mob_total_wide, 0)
|
||||||
|
|
||||||
mcl_log("mob_type", mob_type)
|
mcl_log("mob_type", mob_type)
|
||||||
mcl_log("cap_space_wide", cap_space_wide)
|
mcl_log("cap_space_wide", cap_space_wide)
|
||||||
|
@ -1005,10 +970,10 @@ if mobs_spawn then
|
||||||
local cap_space_available = 0
|
local cap_space_available = 0
|
||||||
if mob_type == "hostile" then
|
if mob_type == "hostile" then
|
||||||
mcl_log("cap_space_global", cap_space_hostile)
|
mcl_log("cap_space_global", cap_space_hostile)
|
||||||
cap_space_available = math.min(cap_space_hostile, cap_space_wide)
|
cap_space_available = math_min(cap_space_hostile, cap_space_wide)
|
||||||
else
|
else
|
||||||
mcl_log("cap_space_global", cap_space_non_hostile)
|
mcl_log("cap_space_global", cap_space_non_hostile)
|
||||||
cap_space_available = math.min(cap_space_non_hostile, cap_space_wide)
|
cap_space_available = math_min(cap_space_non_hostile, cap_space_wide)
|
||||||
end
|
end
|
||||||
|
|
||||||
local mob_total_close = mob_counts_close[mob_type]
|
local mob_total_close = mob_counts_close[mob_type]
|
||||||
|
@ -1017,8 +982,8 @@ if mobs_spawn then
|
||||||
mob_total_close = 0
|
mob_total_close = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local cap_space_close = math.max(close_zone_cap - mob_total_close, 0)
|
local cap_space_close = math_max(close_zone_cap - mob_total_close, 0)
|
||||||
cap_space_available = math.min(cap_space_available, cap_space_close)
|
cap_space_available = math_min(cap_space_available, cap_space_close)
|
||||||
|
|
||||||
mcl_log("cap_space_close", cap_space_close)
|
mcl_log("cap_space_close", cap_space_close)
|
||||||
mcl_log("cap_space_available", cap_space_available)
|
mcl_log("cap_space_available", cap_space_available)
|
||||||
|
@ -1145,7 +1110,7 @@ if mobs_spawn then
|
||||||
|
|
||||||
local amount_to_spawn = math.random(group_min, spawn_in_group)
|
local amount_to_spawn = math.random(group_min, spawn_in_group)
|
||||||
mcl_log("Spawning quantity: " .. amount_to_spawn)
|
mcl_log("Spawning quantity: " .. amount_to_spawn)
|
||||||
amount_to_spawn = math.min(amount_to_spawn, cap_space_available)
|
amount_to_spawn = math_min(amount_to_spawn, cap_space_available)
|
||||||
mcl_log("throttled spawning quantity: " .. amount_to_spawn)
|
mcl_log("throttled spawning quantity: " .. amount_to_spawn)
|
||||||
|
|
||||||
if logging then
|
if logging then
|
||||||
|
@ -1196,8 +1161,8 @@ if mobs_spawn then
|
||||||
local players = get_connected_players()
|
local players = get_connected_players()
|
||||||
local total_mobs, total_non_hostile, total_hostile = count_mobs_total_cap()
|
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_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)
|
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_hostile", cap_space_hostile)
|
||||||
mcl_log("global cap_space_non_hostile", cap_space_non_hostile)
|
mcl_log("global cap_space_non_hostile", cap_space_non_hostile)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue