237 lines
7.7 KiB
Lua

-- When trying to find a safe spot, the mob makes multiple raycasts
-- from the mob all around the mob horizontally. This number is
-- the angle difference in degrees between each ray.
local FIND_LAND_ANGLE_STEP = 15
rp_mobs_mobs.microtasks = {}
rp_mobs_mobs.tasks = {}
rp_mobs_mobs.task_queues = {}
-- Returns true if the given node (by node name) is a liquid
rp_mobs_mobs.is_liquid = function(nodename)
local ndef = minetest.registered_nodes[nodename]
return ndef and (ndef.liquid_move_physics == true or (ndef.liquid_move_physics == nil and ndef.liquidtype ~= "none"))
end
-- Returns true if node deals damage
rp_mobs_mobs.is_damaging = function(nodename)
local ndef = minetest.registered_nodes[nodename]
return ndef and ndef.damage_per_second > 0
end
-- Returns true if node is walkable
rp_mobs_mobs.is_walkable = function(nodename)
local ndef = minetest.registered_nodes[nodename]
return ndef and ndef.walkable
end
-- Returns true if node is climbable
rp_mobs_mobs.is_climbable = function(nodename)
local ndef = minetest.registered_nodes[nodename]
return ndef and ndef.climbable
end
-- Returns true if the node(s) in front of the mob are safe.
-- This is considered unsafe:
-- * damage_per_second > 0
-- * drowning > 0
-- * a drop, if greater than cliff_depth
-- * a drop on a node with high fall_damage_add_percent
--
-- Parameters:
-- * mob: Mob object to check
-- * cliff_depth: How deep the mob is allowed to fall
-- * max_fall_damage_add_percent_drop_on: (optional): If set, mob can
-- not fall on a node with a fall_damage_add_percent group that is higher or equal than this value
rp_mobs_mobs.is_front_safe = function(mob, cliff_depth, max_fall_damage_add_percent_drop_on)
local vel = mob.object:get_velocity()
vel.y = 0
local yaw = mob.object:get_yaw()
local dir = vector.normalize(vel)
if vector.length(dir) > 0.5 then
yaw = minetest.dir_to_yaw(dir)
else
yaw = mob.object:get_yaw()
dir = minetest.yaw_to_dir(yaw)
end
local pos = mob.object:get_pos()
if mob._front_body_point then
local fbp = table.copy(mob._front_body_point)
fbp = vector.rotate_around_axis(fbp, vector.new(0, 1, 0), yaw)
pos = vector.add(pos, fbp)
end
local pos_front = vector.add(pos, dir)
local node_front = minetest.get_node(pos_front)
local def_front = minetest.registered_nodes[node_front.name]
if def_front and (def_front.drowning > 0 or def_front.damage_per_second > 0) then
return false
end
if def_front and not def_front.walkable then
local safe_drop = false
for c=1, cliff_depth do
local cpos = vector.add(pos_front, vector.new(0, -c, 0))
local cnode = minetest.get_node(cpos)
local cdef = minetest.registered_nodes[cnode.name]
if not cdef then
-- Unknown node
return false
elseif cdef.drowning > 0 then
return false
elseif cdef.damage_per_second > 0 then
return false
elseif cdef.walkable then
-- Mob doesn't like to land on node with high fall damage addition
if max_fall_damage_add_percent_drop_on and c > 1 and minetest.get_item_group(cnode.name, "fall_damage_add_percent") >= max_fall_damage_add_percent_drop_on then
return false
else
safe_drop = true
break
end
end
end
if not safe_drop then
return false
end
end
return true
end
-- This function helps the mob find safe land from a lake or ocean.
--
-- Assuming that pos is a position above a large body of
-- liquid (like a lake or ocean), this function can return
-- the (approximately) closest position of walkable land
-- from that position, up to a hardcoded maximum range.
--
--
-- Argument:
-- * pos: Start position
-- * find_land_length: How far the mob looks away for safe land (raycast length)
--
-- returns: <position>, <angle from position>
-- or nil, nil if no position found
rp_mobs_mobs.find_land_from_liquid = function(pos, find_land_length)
local startpos = table.copy(pos)
startpos.y = startpos.y - 1
local startnode = minetest.get_node(startpos)
if not rp_mobs_mobs.is_liquid(startnode.name) then
startpos.y = startpos.y - 1
end
local vec_y = vector.new(0, 1, 0)
local best_pos
local best_dist
local best_angle
for angle=0, 359, FIND_LAND_ANGLE_STEP do
local angle_rad = (angle/360) * (math.pi*2)
local vec = vector.new(0, 0, 1)
vec = vector.rotate_around_axis(vec, vec_y, angle_rad)
vec = vector.multiply(vec, find_land_length)
local rc = minetest.raycast(startpos, vector.add(startpos, vec), false, false)
for pt in rc do
if pt.type == "node" then
local dist = vector.distance(startpos, pt.under)
local up = vector.add(pt.under, vector.new(0, 1, 0))
local upnode = minetest.get_node(up)
if not best_dist or dist < best_dist then
-- Ignore if ray collided with overhigh selection boxes (kelp, seagrass, etc.)
if pt.intersection_point.y - 0.5 < pt.under.y and
-- Node above must be non-walkable
not rp_mobs_mobs.is_walkable(upnode.name) then
best_pos = up
best_dist = dist
local pos1 = vector.copy(startpos)
local pos2 = vector.copy(up)
pos1.y = 0
pos2.y = 0
best_angle = minetest.dir_to_yaw(vector.direction(pos1, pos2))
break
end
end
if rp_mobs_mobs.is_walkable(upnode.name) then
break
end
end
end
end
return best_pos, best_angle
end
-- Arguments:
-- * pos: Start position
-- * find_land_length: How far the mob looks away for safe land (raycast length)
--
-- returns: <position>, <angle from position>
-- or nil, nil if no position found
rp_mobs_mobs.find_safe_node_from_pos = function(pos, find_land_length)
local startpos = table.copy(pos)
startpos.y = math.floor(startpos.y)
startpos.y = startpos.y - 1
local startnode = minetest.get_node(startpos)
local best_pos
local best_dist
local best_angle
local vec_y = vector.new(0, 1, 0)
for angle=0, 359, FIND_LAND_ANGLE_STEP do
local angle_rad = (angle/360) * (math.pi*2)
local vec = vector.new(0, 0, 1)
vec = vector.rotate_around_axis(vec, vec_y, angle_rad)
vec = vector.multiply(vec, find_land_length)
local rc = minetest.raycast(startpos, vector.add(startpos, vec), false, false)
for pt in rc do
if pt.type == "node" then
local floor = pt.under
local floornode = minetest.get_node(floor)
local up = vector.add(floor, vector.new(0, 1, 0))
local upnode = minetest.get_node(up)
if rp_mobs_mobs.is_walkable(floornode.name) then
if rp_mobs_mobs.is_walkable(upnode.name) then
break
elseif not rp_mobs_mobs.is_walkable(upnode.name) and not rp_mobs_mobs.is_damaging(upnode.name) then
local dist = vector.distance(startpos, floor)
if not best_dist or dist < best_dist then
best_pos = up
best_dist = dist
local pos1 = vector.copy(startpos)
local pos2 = vector.copy(up)
pos1.y = 0
pos2.y = 0
best_angle = minetest.dir_to_yaw(vector.direction(pos1, pos2))
end
break
end
end
end
end
end
return best_pos, best_angle
end
-- Add a "stand still" task to the mob's task queue with
-- an optional yaw
rp_mobs_mobs.add_halt_to_task_queue = function(task_queue, mob, set_yaw, idle_min, idle_max)
local mt_sleep = rp_mobs.microtasks.sleep(math.random(idle_min, idle_max)/1000)
mt_sleep.start_animation = "idle"
local task = rp_mobs.create_task({label="stand still"})
local vel = mob.object:get_velocity()
vel.x = 0
vel.z = 0
local yaw
if not set_yaw then
yaw = mob.object:get_yaw()
else
yaw = set_yaw
end
local mt_yaw = rp_mobs.microtasks.set_yaw(yaw)
local mt_acceleration = rp_mobs.microtasks.set_acceleration(rp_mobs.GRAVITY_VECTOR)
rp_mobs.add_microtask_to_task(mob, mt_acceleration, task)
if set_yaw then
rp_mobs.add_microtask_to_task(mob, mt_yaw, task)
end
rp_mobs.add_microtask_to_task(mob, rp_mobs.microtasks.move_straight(vel, yaw, vector.new(0.5,0,0.5), 1), task)
rp_mobs.add_microtask_to_task(mob, mt_sleep, task)
rp_mobs.add_task_to_task_queue(task_queue, task)
end