2024-03-20 15:31:47 +01:00

487 lines
16 KiB
Lua

-- Scan for players/mobs and update the following state every this many seconds
local FOLLOW_CHECK_TIME = 1.0
-- 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
-- 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 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
-- This function creates and returns a microtask that scans the
-- mob's surroundings within view_range for players.
-- The result is stored in mob._temp_custom_state.follow_player.
-- This microtask only *searches* for suitable targets to follow,
-- it does *NOT* actually follow them. Other microtasks
-- are supposed to decide what do do with this information.
-- Parameters:
-- * view_range: Range in which mob can detect players
rp_mobs_mobs.microtask_player_find_follow = function(view_range)
return rp_mobs.create_microtask({
label = "find player to follow",
on_start = function(self, mob)
self.statedata.timer = 0
end,
on_step = function(self, mob, dtime)
-- Perform the follow check periodically
self.statedata.timer = self.statedata.timer + dtime
if self.statedata.timer < FOLLOW_CHECK_TIME then
return
end
self.statedata.timer = 0
local s = mob.object:get_pos()
if (mob._temp_custom_state.follow_player == nil) then
-- Mark closest player within view range as player to follow
local p, dist
local min_dist, closest_player
local objs = minetest.get_objects_inside_radius(s, view_range)
for o=1, #objs do
local obj = objs[o]
if obj:is_player() and obj:get_hp() > 0 then
local player = obj
p = player:get_pos()
dist = vector.distance(s, p)
if dist <= view_range and ((not min_dist) or dist < min_dist) then
min_dist = dist
closest_player = player
break
end
end
end
if closest_player then
mob._temp_custom_state.follow_player = closest_player:get_player_name()
end
else
-- Unfollow player if out of view range, dead or gone
local player = minetest.get_player_by_name(mob._temp_custom_state.follow_player)
if player then
local p = player:get_pos()
local dist = vector.distance(s, p)
-- Out of range
if dist > view_range then
mob._temp_custom_state.follow_player = nil
elseif player:get_hp() == 0 then
mob._temp_custom_state.follow_player = nil
end
else
mob._temp_custom_state.follow_player = nil
end
end
end,
is_finished = function()
return false
end,
})
end
-- Creates and returns a task queue that exclusively performs the
-- 'player_find_follow' microtask. Provided for convenience.
-- See `rp_mobs_mobs.microtask_player_find_follow` for details.
-- Parameters:
-- * view_range: Range in which mob can detect players
rp_mobs_mobs.task_queue_player_follow_scan = function(view_range)
local decider = function(task_queue, mob)
local task = rp_mobs.create_task({label="scan for entities to follow"})
local mt_find_follow = rp_mobs_mobs.microtask_player_find_follow(view_range)
rp_mobs.add_microtask_to_task(mob, mt_find_follow, task)
rp_mobs.add_task_to_task_queue(task_queue, task)
end
local tq = rp_mobs.create_task_queue(decider)
return tq
end
-- This function creates and returns a microtask that scans the
-- mob's surroundings within view_range for other interesting entities:
-- 1) Players holding food
-- 2) Mobs of same species to mate with
-- The result is stored in mob._temp_custom_state.follow_partner
-- and mob._temp_custom_state.follow_player.
-- This microtask only *searches* for suitable targets to follow,
-- it does *NOT* actually follow them. Other microtasks
-- are supposed to decide what do do with this information.
-- Parameters:
-- * view_range: Range in which mob can detect other objects
-- * food_list: List of food items the mob likes to follow (itemstrings)
rp_mobs_mobs.microtask_food_breed_find_follow = function(view_range, food_list)
return rp_mobs.create_microtask({
label = "find entities to follow (partners and players holding food)",
on_start = function(self, mob)
self.statedata.timer = 0
end,
on_step = function(self, mob, dtime)
-- Perform the follow check periodically
self.statedata.timer = self.statedata.timer + dtime
if self.statedata.timer < FOLLOW_CHECK_TIME then
return
end
self.statedata.timer = 0
local s = mob.object:get_pos()
local objs = minetest.get_objects_inside_radius(s, view_range)
-- Look for other horny mob nearby
if mob._horny then
if mob._temp_custom_state.follow_partner == nil then
local min_dist, closest_partner
local min_dist_h, closest_partner_h
for o=1, #objs do
local obj = objs[o]
local ent = obj:get_luaentity()
-- Find other mob of same species
if obj ~= mob.object and ent and ent._cmi_is_mob and ent.name == mob.name and not ent._child then
local p = obj:get_pos()
local dist = vector.distance(s, p)
-- Find closest one
if dist <= view_range then
-- Closest partner
if ((not min_dist) or dist < min_dist) then
min_dist = dist
closest_partner = obj
end
-- Closest horny partner
if ent._horny and ((not min_dist_h) or dist < min_dist_h) then
min_dist_h = dist
closest_partner_h = obj
end
end
end
end
-- Set new partner to follow (prefer horny)
if closest_partner_h then
mob._temp_custom_state.follow_partner = closest_partner_h
elseif closest_partner then
mob._temp_custom_state.follow_partner = closest_partner
end
-- Unfollow partner if out of range
elseif mob._temp_custom_state.follow_partner:get_luaentity() then
local p = mob._temp_custom_state.follow_partner:get_pos()
local dist = vector.distance(s, p)
-- Out of range
if dist > view_range then
mob._temp_custom_state.follow_partner = nil
end
else
-- Partner object is gone
mob._temp_custom_state.follow_partner = nil
end
else
-- Unfollow partner if no longer horny
mob._temp_custom_state.follow_partner = nil
end
if (mob._temp_custom_state.follow_player == nil) then
-- Mark closest player holding food within view range as player to follow
local p, dist
local min_dist, closest_player
for o=1, #objs do
local obj = objs[o]
if obj:is_player() then
local player = obj
p = player:get_pos()
dist = vector.distance(s, p)
if dist <= view_range and ((not min_dist) or dist < min_dist) then
local wield = player:get_wielded_item()
-- Is holding food?
for f=1, #food_list do
if wield:get_name() == food_list[f] then
min_dist = dist
closest_player = player
break
end
end
end
end
end
if closest_player then
mob._temp_custom_state.follow_player = closest_player:get_player_name()
end
else
-- Unfollow player if out of view range or not holding food
local player = minetest.get_player_by_name(mob._temp_custom_state.follow_player)
if player then
local p = player:get_pos()
local dist = vector.distance(s, p)
-- Out of range
if dist > view_range then
mob._temp_custom_state.follow_player = nil
else
local wield = player:get_wielded_item()
for f=1, #food_list do
if wield:get_name() == food_list[f] then
return
end
end
-- Not holding food
mob._temp_custom_state.follow_player = nil
return
end
end
end
end,
is_finished = function()
return false
end,
})
end
-- Creates and returns a task queue that exclusively performs the 'find_follow'
-- microtask. Provided for convenience.
-- See `rp_mobs_mobs.microtask_food_breed_find_follow` for details.
-- Parameters:
-- * view_range: Range in which mob can detect other objects
-- * food_list: List of food items the mob likes to follow (itemstrings)
rp_mobs_mobs.task_queue_food_breed_follow_scan = function(view_range, food_list)
local decider = function(task_queue, mob)
local task = rp_mobs.create_task({label="scan for entities to follow"})
local mt_find_follow = rp_mobs_mobs.microtask_food_breed_find_follow(view_range, food_list)
rp_mobs.add_microtask_to_task(mob, mt_find_follow, task)
rp_mobs.add_task_to_task_queue(task_queue, task)
end
local tq = rp_mobs.create_task_queue(decider)
return tq
end
-- Creates and returns a task queue that randomly plays the mob's 'call'
-- sound from time to time.
-- Parameters:
-- * sound_timer_min: Minimum time between call sounds (milliseconds)
-- * sound_timer_max: Maximum time between call sounds (milliseconds)
rp_mobs_mobs.task_queue_call_sound = function(sound_timer_min, sound_timer_max)
local decider = function(task_queue, mob)
local task = rp_mobs.create_task({label="random call sound"})
local mt_sleep = rp_mobs.microtasks.sleep(math.random(sound_timer_min, sound_timer_max)/1000)
local mt_call = rp_mobs.create_microtask({
label = "play call sound",
singlestep = true,
on_step = function(self, mob, dtime)
rp_mobs.default_mob_sound(mob, "call", false)
end
})
rp_mobs.add_microtask_to_task(mob, mt_sleep, task)
rp_mobs.add_microtask_to_task(mob, mt_call, task)
rp_mobs.add_task_to_task_queue(task_queue, task)
end
local tq = rp_mobs.create_task_queue(decider)
return tq
end