---------------
-- Behaviors --
---------------

-- Math --

local abs = math.abs
local random = math.random
local ceil = math.ceil
local floor = math.floor
local rad = math.rad

local function average(t)
    local sum = 0
    for _,v in pairs(t) do -- Get the sum of all numbers in t
      sum = sum + v
    end
    return sum / #t
end

local function clamp(val, min, max)
	if val < min then
		val = min
	elseif max < val then
		val = max
	end
	return val
end

-- Vector Math --

local vec_dist = vector.distance
local vec_dir = vector.direction
local vec_sub = vector.subtract
local vec_add = vector.add
local vec_multi = vector.multiply
local vec_normal = vector.normalize

local function vec_raise(v, n)
    return {x = v.x, y = v.y + n, z = v.z}
end

local yaw2dir = minetest.yaw_to_dir
local dir2yaw = minetest.dir_to_yaw

--------------
-- Settings --
--------------

------------
-- Tables --
------------

local is_flyable = {}
local is_liquid = {}
local is_solid = {}

minetest.register_on_mods_loaded(function()
    for name in pairs(minetest.registered_nodes) do
        if name ~= "air" and name ~= "ignore" then
            if minetest.registered_nodes[name].walkable
            or minetest.registered_nodes[name].drawtype == "liquid" then
                is_flyable[name] = true
                if minetest.registered_nodes[name].walkable then
                    is_solid[name] = true
                else
                    is_liquid[name] = true
                end
            end
        end
    end
end)

---------------------
-- Local Utilities --
---------------------

local moveable = creatura.is_pos_moveable
local fast_ray_sight = creatura.fast_ray_sight

local function get_ground_level(pos2, max_height)
    local node = minetest.get_node(pos2)
    local node_under = minetest.get_node({
        x = pos2.x,
        y = pos2.y - 1,
        z = pos2.z
    })
    local height = 0
    local walkable = is_solid[node_under.name] and not is_solid[node.name]
    if walkable then
        return pos2
    elseif not walkable then
        if not is_solid[node_under.name] then
            while not is_solid[node_under.name]
            and height < max_height do
                pos2.y = pos2.y - 1
                node_under = minetest.get_node({
                    x = pos2.x,
                    y = pos2.y - 1,
                    z = pos2.z
                })
                height = height + 1
            end
        else
            while is_solid[node.name]
            and height < max_height do
                pos2.y = pos2.y + 1
                node = minetest.get_node(pos2)
                height = height + 1
            end
        end
        return pos2
    end
end

local function get_ceiling_positions(pos, range)
    local walkable = minetest.find_nodes_in_area(
        {x = pos.x + range, y = pos.y + range, z = pos.z + range},
        {x = pos.x - range, y = pos.y, z = pos.z - range},
        animalia.walkable_nodes
    )
    if #walkable < 1 then return {} end
    local output = {}
    for i = 1, #walkable do
        local i_pos = walkable[i]
        local under = {
            x = i_pos.x,
            y = i_pos.y - 1,
            z = i_pos.z
        }
        if minetest.get_node(under).name == "air"
        and is_solid[minetest.get_node(i_pos).name] then
            table.insert(output, i_pos)
        end
    end
    return output
end

local function get_obstacle_avoidance(self, lift)
    local pos = self.object:get_pos()
    local yaw = self.object:get_yaw()
    pos.y = pos.y + self.stepheight
    local vel = self.object:get_velocity()
    local vel_len = abs(vector.length(vel))
    if vel_len < 1.5 then
        vel_len = 1.5
    end
    local dir = yaw2dir(yaw)
    dir.y = lift
    local outset = vec_add(pos, vec_multi(dir, vel_len))
    local pos2
    local obstacle = false
    if not fast_ray_sight(pos, outset) then
        pos2 = vec_add(pos, vec_multi(dir, -vel_len))
        obstacle = true
    end
    return pos2, obstacle
end

local function get_wander_pos_3d(self, range)
    local outset = random(range or 4)
    local pos = self.object:get_pos()
    local move_dir = {
        x = random(-10, 10) * 0.1,
        z = random(-10, 10) * 0.1,
        y = random(-10, 10) * 0.1
    }
    local pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), random(1, outset)))
    local sight, dist = fast_ray_sight(pos, pos2, true)
    if not sight then
        pos2 = vec_add(pos, vec_multi(vec_normal(move_dir), dist - 0.5))
    end
    return pos2
end

local function get_boid_members(pos, radius, name, texture_no)
    local objects = minetest.get_objects_inside_radius(pos, radius)
    if #objects < 2 then return {} end
    local members = {}
    local max_boid = minetest.registered_entities[name].max_boids or 7
    for i = 1, #objects do
        if #members > max_boid then break end
        local object = objects[i]
        if object:get_luaentity()
        and object:get_luaentity().name == name 
        and object:get_luaentity().texture_no == texture_no  then
            object:get_luaentity().boid_heading = rad(random(360))
            table.insert(members, object)
        end
    end
    return members
end

----------------------
-- Movement Methods --
----------------------

local function movement_fly(self, pos2)
    -- Initial Properties
    local pos = self.object:get_pos()
    local turn_rate = self.turn_rate or 10
    local speed = self.speed or 2
    self:animate("fly")
    self:set_gravity(0)
    -- Collision Avoidance
    local temp_goal = self._movement_data.temp_goal
    local obstacle = self._movement_data.obstacle or false
    if not temp_goal
    or self:pos_in_box(temp_goal, self.width) then
        self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y)
    end
    local neighbor = self._movement_data.temp_goal
    -- Calculate Movement
    local dir = vector.direction(pos, pos2)
    local tyaw = minetest.dir_to_yaw(dir)
    if neighbor then
        local lift = dir.y
        dir = vector.direction(pos, neighbor)
        if not obstacle then
            dir.y = lift
        end
        tyaw = minetest.dir_to_yaw(dir)
    end
    if self._path
    and #self._path > 1 then
        neighbor = self._path[2]
        dir = vector.direction(pos, neighbor)
        tyaw = minetest.dir_to_yaw(dir)
        if self:pos_in_box(neighbor, self.width + 0.2) then
            table.remove(self._path, 1)
        end
    else
        self._path = creatura.find_path(self, pos, pos2, self.width, self.height, 300, false, true)
    end
    -- Apply Movement
    self:turn_to(boid_angle or tyaw, turn_rate)
    self:set_forward_velocity(speed)
    local v_speed = speed * dir.y
    local vel = self.object:get_velocity()
    vel.y = vel.y + (v_speed - vel.y) * 0.2
    self:set_vertical_velocity(vel.y)
    if self:pos_in_box(pos2) then
        self:halt()
    end
end

creatura.register_movement_method("animalia:fly_path", movement_fly)

local function movement_fly_waypoints(self, pos2, speed)
    -- Initial Properties
    local pos = self.object:get_pos()
    self:animate("fly")
    self:set_gravity(0)
    -- Collision Avoidance
    local temp_goal = self._movement_data.temp_goal
    local obstacle = self._movement_data.obstacle or false
    if not temp_goal
    or self:pos_in_box(temp_goal, 0.4) then
        self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y)
    end
    local neighbor = self._movement_data.temp_goal
    -- Calculate Movement
    local dir = vector.direction(pos, pos2)
    local tyaw = minetest.dir_to_yaw(dir)
    local turn_rate = self.turn_rate or 10
    local speed = self.speed or 2
    if neighbor then
        local lift = dir.y
        dir = vector.direction(pos, neighbor)
        if not obstacle then
            dir.y = lift
        end
        tyaw = minetest.dir_to_yaw(dir)
    end
    -- Apply Movement
    self:turn_to(boid_angle or tyaw, turn_rate)
    self:set_forward_velocity(speed)
    local v_speed = speed * dir.y
    local vel = self.object:get_velocity()
    vel.y = vel.y + (v_speed - vel.y) * 0.2
    self:set_vertical_velocity(vel.y)
    if self:pos_in_box(pos2) then
        self:halt()
    end
end

creatura.register_movement_method("animalia:fly_waypoints", movement_fly_waypoints)

-- Fly Obstacle Avoidance --

local function movement_fly_obstacle_avoidance(self, pos2, speed)
    -- Initial Properties
    local pos = self.object:get_pos()
    local turn_rate = self.turn_rate or 10
    local speed = self.speed or 2
    self:animate("fly")
    self:set_gravity(0)
    -- Collision Avoidance
    local temp_goal = self._movement_data.temp_goal
    local obstacle = self._movement_data.obstacle or false
    local timer = self._movement_data.timer
    if not temp_goal
    or self:pos_in_box(temp_goal, 0.4)
    or (timer
    and timer <= 0) then
        self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y)
        temp_goal = self._movement_data.temp_goal
        obstacle = self._movement_data.obstacle or false
        if temp_goal then
            self._movement_data.timer = 3
        end
    end
    if timer then
        self._movement_data.timer = self._movement_data.timer - self.dtime
    end
    -- Calculate Movement
    local dir = vector.direction(pos, pos2)
    local tyaw = minetest.dir_to_yaw(dir)
    if temp_goal
    and obstacle then
        dir = vector.direction(pos, temp_goal)
        dir.y = vec_dir(pos, pos2).y
        tyaw = minetest.dir_to_yaw(dir)
    end
    -- Apply Movement
    self:turn_to(tyaw, turn_rate)
    self:set_forward_velocity(speed)
    local v_speed = (speed) * dir.y
    local vel = self.object:get_velocity()
    vel.y = vel.y + (v_speed - vel.y) * 0.2
    self:set_vertical_velocity(vel.y)
    if self:pos_in_box(pos2, 0.5) then
        self:halt()
    end
end

creatura.register_movement_method("animalia:fly_obstacle_avoidance", movement_fly_obstacle_avoidance)

-- Swimming --

local function movement_swim_obstacle_avoidance(self, pos2, speed)
    -- Initial Properties
    local pos = self.object:get_pos()
    self:animate("swim")
    self:set_gravity(0)
    -- Collision Avoidance
    local temp_goal = self._movement_data.temp_goal
    local obstacle = self._movement_data.obstacle or false
    local timer = self._movement_data.timer
    if not temp_goal
    or self:pos_in_box(temp_goal, 0.4)
    or (timer
    and timer <= 0) then
        self._movement_data.temp_goal, self._movement_data.obstacle = get_obstacle_avoidance(self, vec_dir(pos, pos2).y)
        temp_goal = self._movement_data.temp_goal
        obstacle = self._movement_data.obstacle or false
        if temp_goal then
            self._movement_data.timer = vec_dist(pos, temp_goal) / self.speed
        end
    end
    if timer then
        self._movement_data.timer = self._movement_data.timer - self.dtime
    end
    -- Calculate Movement
    local dir = vector.direction(pos, pos2)
    local tyaw = minetest.dir_to_yaw(dir)
    if temp_goal
    and obstacle then
        dir = vector.direction(pos, temp_goal)
        tyaw = minetest.dir_to_yaw(dir)
    end
    -- Apply Movement
    self:turn_to(tyaw, turn_rate)
    self:set_forward_velocity(speed)
    local v_speed = speed * dir.y
    local vel = self.object:get_velocity()
    vel.y = vel.y + (v_speed - vel.y) * 0.2
    if not is_liquid[minetest.get_node(vec_raise(pos, 1)).name]
    and vel.y > 0 then
        vel.y = 0
    end
    self:set_vertical_velocity(vel.y)
    if self:pos_in_box(pos2) then
        self:halt()
    end
end

creatura.register_movement_method("animalia:swim_obstacle_avoidance", movement_swim_obstacle_avoidance)

-------------
-- Actions --
-------------

function animalia.action_fall(self)
    local function func(self)
        self:animate("fall")
        self:set_gravity(-1)
        local vel = self.object:get_velocity()
        if vel.y < -3.8 then
            self:set_vertical_velocity(-0.1)
        end
        self._fall_start = nil
        if self.touching_ground then
            return true
        end
    end
    self:set_action(func)
end

function animalia.action_punch(self, target)
    local function func(self)
        if not creatura.is_alive(target) then
            return true
        end
        local yaw = self.object:get_yaw()
        local pos = self.object:get_pos()
        local tpos = target:get_pos()
        local dir = vector.direction(pos, tpos)
        local tyaw = minetest.dir_to_yaw(dir)
        self:turn_to(tyaw)
        if self.touching_ground then
            self:animate("leap")
            local jump_vel = vec_multi(dir, self.speed)
            jump_vel.y = 3
            self.object:add_velocity(jump_vel)
        end
        if vec_dist(pos, tpos) < 2 then
            self:punch_target(target)
            return true
        end
    end
    self:set_action(func)
end

function animalia.action_latch_to_ceil(self, time, anim)
    local timer = time
    local function func(self)
        self:halt()
        self:set_forward_velocity(0)
        self:set_vertical_velocity(9)
        self:set_gravity(3)
        self:animate(anim or "latch")
        timer = timer - self.dtime
        if timer <= 0 then
            return true
        end
    end
    self:set_action(func)
end

function animalia.action_boid_move(self, pos2, timeout, method)
    local boids = get_boid_members(self.object:get_pos(), 6, self.name, self.texture_no)
    local timer = timeout
    local goal = pos2
    local function func(self)
        local pos = self.object:get_pos()
        timer = timer - self.dtime
        if #boids > 2 then
            local boid_angle, boid_lift = creatura.get_boid_angle(self, boids, 6)
            if boid_angle then
                local dir2goal = vec_dir(pos, pos2)
                local yaw2goal = minetest.dir_to_yaw(dir2goal)
                boid_angle = boid_angle + (yaw2goal - boid_angle) * 0.15
                local boid_dir = minetest.yaw_to_dir(boid_angle)
                if boid_lift then
                    boid_dir.y = boid_lift + (vec_dir(pos, goal).y - boid_lift) * 0.5
                else
                    boid_dir.y = vec_dir(pos, goal).y
                end
                pos2 = vec_add(pos, vec_multi(boid_dir, 4))
            end
        end
        if timer <= 0
        or self:pos_in_box(pos2, 0.25) then
            self:halt()
            return true
        end
        self:move(pos2, method or "animalia:fly_obstacle_avoidance", 1)
    end
    self:set_action(func)
end

function animalia.action_boid_walk(self, pos2, timeout, method, speed_factor)
    local boids = creatura.get_boid_members(self.object:get_pos(), 12, self.name)
    local timer = timeout
    local move_init = false
    local goal = pos2
    local function func(self)
        local pos = self.object:get_pos()
        timer = timer - self.dtime
        if #boids > 2 then
            local boid_angle = creatura.get_boid_angle(self, boids, 12)
            if boid_angle then
                local dir2goal = vec_dir(pos, goal)
                local yaw2goal = minetest.dir_to_yaw(dir2goal)
                boid_angle = boid_angle + (yaw2goal - boid_angle) * 0.15
                local boid_dir = minetest.yaw_to_dir(boid_angle)
                pos2 = get_ground_level(vec_add(pos, vec_multi(boid_dir, 4)), 2)
            end
        end
        if not pos2
        or (move_init
        and not self._movement_data.goal) then
            return true
        end
        if timer <= 0
        or self:pos_in_box({x = goal.x, y = pos.y + 0.1, z = goal.z})
        or vec_dist(pos, goal) < 1 then
            self:halt()
            return true
        end
        self:move(pos2, method or "creatura:obstacle_avoidance", speed_factor or 1)
        move_init = true
    end
    self:set_action(func)
end

function animalia.action_swim(self, pos, timeout, method, speed_factor, anim)
    local timer = timeout or 4
    local function func(self)
        timer = timer - self.dtime
        if timer <= 0
        or self:pos_in_box(pos) then
            self:halt()
            self:set_gravity(0)
            return true
        end
        self:move(pos, method or "animalia:swim_obstacle_avoidance", speed_factor or 0.5, anim)
        self:set_gravity(0)
    end
    self:set_action(func)
end

---------------
-- Behaviors --
---------------

------------------------
-- Register Utilities --
------------------------

-- Wander

creatura.register_utility("animalia:wander", function(self, group)
    local idle_time = 3
    local move_probability = 3
    local group_tick = 0
    local far_from_group = false
    local function func(self)
        local pos = self.object:get_pos()
        local random_goal = vector.new(
            pos.x + random(-4, 4),
            pos.y,
            pos.z + random(-4, 4)
        )
        if group
        and self:timer(3) then
            local range = self.tracking_range * 0.5
            local group_positions = animalia.get_group_positions(self.name, pos, range + 1)
            if #group_positions > 2 then
                local center = animalia.get_average_pos(group_positions)
                if center
                and (vec_dist(random_goal, center) > range
                or vec_dist(pos, center) > range) then
                    random_goal = center
                    far_from_group = true
                else
                    far_from_group = false
                end
            end
            group_tick = 2
        end
        if not self:get_action() then
            local node = minetest.get_node(random_goal)
            if minetest.registered_nodes[node.name].drawtype == "liquid"
            or minetest.registered_nodes[node.name].walkable then
                random_goal = nil
            end
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                random_goal = self.lasso_pos
            end
            if (random(move_probability) < 2
            and random_goal)
            or far_from_group then
                creatura.action_walk(self, random_goal)
            else
                creatura.action_idle(self, idle_time)
            end
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:skittish_boid_wander", function(self)
    local idle_time = 3
    local move_probability = 3
    local group_tick = 0
    local force_move = false
    local function func(self)
        local pos = self.object:get_pos()
        local random_goal = vector.new(
            pos.x + random(-4, 4),
            pos.y,
            pos.z + random(-4, 4)
        )
        if self:timer(3) then
            local range = self.tracking_range * 0.5
            local group_positions = animalia.get_group_positions(self.name, pos, range + 1)
            if #group_positions > 2 then
                local center = animalia.get_average_pos(group_positions)
                if center
                and (vec_dist(random_goal, center) > range
                or vec_dist(pos, center) > range) then
                    random_goal = center
                    force_move = true
                else
                    force_move = false
                end
            else
                force_move = false
            end
            group_tick = 2
            local player = creatura.get_nearby_player(self)
            if player then
                local target_alive, line_of_sight, player_pos = self:get_target(player)
                if target_alive
                and line_of_sight
                and vec_dist(pos, player_pos) < 8 then
                    force_move = true
                    local dir = vec_dir(pos, player_pos)
                    random_goal = vec_add(pos, vec_multi(vec_add(dir, {x = random(-10, 10) * 0.1, y = 0, z = random(-10, 10) * 0.1}), -3))
                end
            end
        end
        if not self:get_action() then
            local node = minetest.get_node(random_goal)
            if minetest.registered_nodes[node.name].drawtype == "liquid"
            or minetest.registered_nodes[node.name].walkable then
                random_goal = nil
                force_move = false
            end
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                random_goal = self.lasso_pos
            end
            if (random(move_probability) < 2
            and random_goal)
            or force_move then
                get_ground_level(random_goal, 1)
                animalia.action_boid_walk(self, random_goal, 6, "creatura:obstacle_avoidance", 0.5)
            else
                creatura.action_idle(self, idle_time)
            end
        end
    end
    self:set_utility(func)
end)


creatura.register_utility("animalia:boid_wander", function(self, group)
    local idle_time = 3
    local move_probability = 5
    local group_tick = 0
    local function func(self)
        local pos = self.object:get_pos()
        local random_goal = vector.new(
            pos.x + random(-4, 4),
            pos.y,
            pos.z + random(-4, 4)
        )
        if group
        and self:timer(3) then
            local range = self.tracking_range * 0.5
            local group_positions = animalia.get_group_positions(self.name, pos, range + 1)
            if #group_positions > 2 then
                local center = animalia.get_average_pos(group_positions)
                if center
                and (vec_dist(random_goal, center) > range
                or vec_dist(pos, center) > range) then
                    random_goal = center
                end
            end
            group_tick = 2
        end
        if not self:get_action() then
            local node = minetest.get_node(random_goal)
            if minetest.registered_nodes[node.name].drawtype == "liquid"
            or minetest.registered_nodes[node.name].walkable then
                random_goal = nil
            end
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                random_goal = self.lasso_pos
            end
            if random(move_probability) < 2
            and random_goal then
                animalia.action_boid_walk(self, random_goal, 6, "creatura:neighbors", 0.25)
            else
                creatura.action_idle(self, idle_time)
            end
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:wander_water_surface", function(self)
    local idle_time = 3
    local move_probability = 3
    local function func(self)
        if not self.in_liquid then return true end
        local pos = self.object:get_pos()
        local random_goal = get_wander_pos_3d(self, 2)
        if not self:get_action() then
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                random_goal = self.lasso_pos
            end
            if random(move_probability) < 2
            and random_goal then
                animalia.action_swim(self, random_goal)
            else
                creatura.action_idle(self, idle_time, "float")
            end
        end
        self:set_gravity(0)
    end
    self:set_utility(func)
end)

-- "Eat" nodes

creatura.register_utility("animalia:eat_from_turf", function(self)
    local function func(self)
        local pos = self.object:get_pos()
        local under = vector.new(pos.x, pos.y - 1, pos.z)
        for _, node in ipairs(self.consumable_nodes) do
            if node.name == minetest.get_node(under).name then
                minetest.set_node(under, {name = node.replacement})
                local def = minetest.registered_nodes[node.name]
                local texture = def.tiles[1]
                texture = texture .. "^[resize:8x8"
                minetest.add_particlespawner({
                    amount = 6,
                    time = 0.1,
                    minpos = vector.new(
                        pos.x - 0.5,
                        pos.y + 0.1,
                        pos.z - 0.5
                    ),
                    maxpos = vector.new(
                        pos.x + 0.5,
                        pos.y + 0.1,
                        pos.z + 0.5
                    ),
                    minvel = {x=-1, y=1, z=-1},
                    maxvel = {x=1, y=2, z=1},
                    minacc = {x=0, y=-5, z=0},
                    maxacc = {x=0, y=-9, z=0},
                    minexptime = 1,
                    maxexptime = 1,
                    minsize = 1,
                    maxsize = 2,
                    collisiondetection = true,
                    vertical = false,
                    texture = texture,
                })
                self.gotten = false
                self:memorize("gotten", self.gotten)
                return true
            else
                return true
            end
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:eat_bug_nodes", function(self)
    local timer = 0.2
    local pos = self.object:get_pos()
    local food = minetest.find_nodes_in_area(vec_sub(pos, 1.5), vec_add(pos, 1.5), self.follow)
    local function func(self)
        pos = self.object:get_pos()
        if food[1] then
            local dist = vector.distance(pos, food[1])
            local dir = vec_dir(pos, food[1])
			local frame = floor(dist * 10)
            self:turn_to(dir2yaw(dir))
			if frame < 15
			and frame > 1 then
                animalia.move_head(self, dir2yaw(dir), dir.y)
                creatura.action_idle(self, 0.1, "tongue_" .. frame)
                timer = timer - self.dtime
            elseif not self:get_action() then
                local pos2 = vec_add(food[1], vec_multi(vec_normal(vec_dir(food[1], pos)), 0.25))
                creatura.action_walk(self, pos2)
            end
        else
            return true
        end
        if timer <= 0
        and food[1] then
            minetest.remove_node(food[1])
            return true
        end
    end
    self:set_utility(func)
end)

-- Escape Water

creatura.register_utility("animalia:swim_to_land", function(self)
    local init = false
    local tpos = nil
    local function func(self)
        if not init then
			for i = 1, 359, 15 do
				local yaw = rad(i)
				local dir = yaw2dir(yaw)
				tpos = animalia.find_collision(self, dir)
				if tpos then
					local node = minetest.get_node({x = tpos.x, y = tpos.y + 1, z = tpos.z})
					 if node.name == "air" then
						break
					 else
						 tpos = nil
					 end
				end
			end
			init = true
		end
        if tpos then
            local pos = self.object:get_pos()
            local yaw = self.object:get_yaw()
            local tyaw = minetest.dir_to_yaw(vec_dir(pos, tpos))
            if abs(tyaw - yaw) > 0.1 then
                self:turn_to(tyaw, 12)
            end
            self:set_gravity(-9.8)
            self:set_forward_velocity(self.speed * 0.66)
            self:animate("walk")
            if vector.distance(pos, tpos) < 1
            or (not self.in_liquid
            and self.touching_ground) then
                return true
            end
        else
            self.liquid_recovery_cooldown = 5
            return true
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:flop", function(self)
    local function func(self)
        if self.in_liquid then
            return true
        end
        if not self:get_action() then
            creatura.action_idle(self, 0.1, "flop")
        end
        self:set_vertical_velocity(0)
        self:set_gravity(-9.8)
    end
    self:set_utility(func)
end)

-- Player Interaction

creatura.register_utility("animalia:flee_from_player", function(self, player, range)
    range = range or self.tracking_range
    local function func(self)
        local target_alive, line_of_sight, tpos = self:get_target(player)
        if not target_alive then return true end
        local pos = self.object:get_pos()
        local dir = vec_dir(pos, tpos)
        local escape_pos = vec_add(pos, vec_multi(vec_add(dir, {x = random(-10, 10) * 0.1, y = 0, z = random(-10, 10) * 0.1}), -3))
        if not self:get_action() then
            escape_pos = get_ground_level(escape_pos, 1)
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                escape_pos = self.lasso_pos
            end
            creatura.action_walk(self, escape_pos, 2, "creatura:obstacle_avoidance", 1, "run")
        end
        if vec_dist(pos, tpos) > range then
            return true
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:boid_flee_from_player", function(self, player, group)
    local mobs_in_group = animalia.get_group(self)
    if group then
        if #mobs_in_group > 0 then
            for i = 1, #mobs_in_group do
                local mob = mobs_in_group[i]
                mob:get_luaentity():initiate_utility("animalia:boid_flee_from_player", mob:get_luaentity(), player)
                mob:get_luaentity():set_utility_score(1)
            end
        end
    end
    local function func(self)
        local target_alive, line_of_sight, tpos = self:get_target(player)
        if not target_alive then return true end
        local pos = self.object:get_pos()
        local dir = vec_dir(pos, tpos)
        local escape_pos = vec_add(pos, vec_multi(vec_add(dir, {x = random(-10, 10) * 0.1, y = 0, z = random(-10, 10) * 0.1}), -3))
        if not self:get_action() then
            escape_pos = get_ground_level(escape_pos, 1)
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                escape_pos = self.lasso_pos
            end
            animalia.action_boid_walk(self, escape_pos, 6, "creatura:obstacle_avoidance", 1)
        end
        if vec_dist(pos, tpos) > self.tracking_range + (#mobs_in_group or 0) then
            return true
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:flee_to_water", function(self)
    local function func(self)
        local pos = self.object:get_pos()
        local water = minetest.find_nodes_in_area_under_air(vec_sub(pos, 3), vec_add(pos, 3), {"default:water_source"})
        if water[1]
        and vec_dist(pos, water[1]) < 0.5 then
            return true
        end
        if water[1]
        and not self:get_action() then
            creatura.action_walk(self, water[1])
        else
            return true
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:follow_player", function(self, player, force)
    local function func(self)
        local player_alive, line_of_sight, tpos = self:get_target(player)
        -- Return if player is dead, not holding food, or behavior isn't forced
        if not player_alive
        or (not self:follow_wielded_item(player)
        and not force) then
            return true
        end
        local pos = self.object:get_pos()
        local dist = vec_dist(pos, tpos)
        if dist > self.tracking_range then
            return true
        end
        if not self:get_action() then
            if dist > self:get_hitbox(self)[4] + 1.5 then
                creatura.action_walk(self, tpos, 6, "creatura:pathfind")
            else
                creatura.action_idle(self, 0.1, "stand")
            end
        end
        self.head_tracking = player
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:sporadic_flee", function(self)
    local timer = 18
    self:clear_action()
    if group then
        local mobs_in_group = animalia.get_group(self)
        if #mobs_in_group > 0 then
            for i = 1, #mobs_in_group do
                local mob = mobs_in_group[i]
                animalia.bh_flee(mob:get_luaentity())
            end
        end
    end
    local function func(self)
        local pos = self.object:get_pos()
        local random_goal = {
            x = pos.x + random(-4, 4),
            y = pos.y,
            z = pos.z + random(-4, 4)
        }
        if not self:get_action() then
            random_goal = get_ground_level(random_goal, 1)
            local node = minetest.get_node(random_goal)
            if minetest.registered_nodes[node.name].drawtype == "liquid"
            or minetest.registered_nodes[node.name].walkable then
                return
            end
            if self.lasso_pos
            and vec_dist(pos, self.lasso_pos) > 10 then
                random_goal = self.lasso_pos
            end
            self._movement_data.speed = self.speed * 1.5
            creatura.action_walk(self, random_goal, 4, "creatura:obstacle_avoidance", 1.5)
        end
        timer = timer - self.dtime
        if timer <= 0 then
            return true
        end
    end
    self:set_utility(func)
end)

-- Mob Interaction

creatura.register_utility("animalia:mammal_breed", function(self)
    local mate = animalia.get_nearby_mate(self, self.name)
    if not mate then self.breeding = false return end
    local breeding_time = 0
    local function func(self)
        if not creatura.is_alive(mate) then
            return true
        end
        local pos = self:get_center_pos()
        local tpos = mate:get_pos()
        local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4])
        if dist < 1.75 then
            breeding_time = breeding_time + self.dtime
        end
        if breeding_time >= 2 then
            if self.gender == "female" then
                for i = 1, self.birth_count or 1 do
                    local object = minetest.add_entity(pos, self.name)
                    local ent = object:get_luaentity()
                    ent.growth_scale = 0.7
                    animalia.initialize_api(ent)
                    animalia.protect_from_despawn(ent)
                end
            end
            self.breeding = false
            self.breeding_cooldown = 300
            self:memorize("breeding", self.breeding)
            self:memorize("breeding_time", self.breeding_time)
            self:memorize("breeding_cooldown", self.breeding_cooldown)
            local minp = vector.subtract(pos, 1)
            local maxp = vec_add(pos, 1)
            animalia.particle_spawner(pos, "heart.png", "float", minp, maxp)
            return true
        end
        if not self:get_action() then
            creatura.action_walk(self, tpos)
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:horse_breed", function(self)
    local mate = animalia.get_nearby_mate(self, self.name)
    if not mate then self.breeding = false return end
    local breeding_time = 0
    local function func(self)
        if not creatura.is_alive(mate) then
            return true
        end
        local pos = self:get_center_pos()
        local tpos = mate:get_pos()
        local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4])
        if dist < 1.75 then
            breeding_time = breeding_time + self.dtime
        end
        if breeding_time >= 2 then
            if self.gender == "female" then
                local object = minetest.add_entity(pos, self.name)
                object:get_luaentity().growth_scale = 0.7
                local ent = object:get_luaentity()
                local tex_no = self.texture_no
                if random(2) < 2 then
                    tex_no = mate:get_luaentity().texture_no
                end
                ent:memorize("texture_no", tex_no)
                ent:memorize("speed", random(mate:get_luaentity().speed, self.speed))
                ent:memorize("jump_power", random(mate:get_luaentity().jump_power, self.jump_power))
                ent:memorize("max_hp", random(mate:get_luaentity().max_hp, self.max_hp))
                ent.speed = ent:recall("speed")
                ent.jump_power = ent:recall("jump_power")
                ent.max_hp = ent:recall("max_hp")
                animalia.initialize_api(ent)
                animalia.protect_from_despawn(ent)
            end
            self.breeding = false
            self.breeding_cooldown = 300
            self:memorize("breeding", self.breeding)
            self:memorize("breeding_time", self.breeding_time)
            self:memorize("breeding_cooldown", self.breeding_cooldown)
            local minp = vector.subtract(pos, 1)
            local maxp = vec_add(pos, 1)
            animalia.particle_spawner(pos, "heart.png", "float", minp, maxp)
            return true
        end
        if not self:get_action() then
            creatura.action_walk(self, tpos)
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:bird_breed", function(self)
    local mate = animalia.get_nearby_mate(self, self.name)
    if not mate then self.breeding = false return end
    local breeding_time = 0
    local function func(self)
        if not creatura.is_alive(mate) then
            return true
        end
        local pos = self:get_center_pos()
        local tpos = mate:get_pos()
        local dist = vec_dist(pos, tpos) - abs(self:get_hitbox(self)[4])
        if dist < 1.75 then
            breeding_time = breeding_time + self.dtime
        end
        if breeding_time >= 2 then
            if self.gender == "female" then
                minetest.add_particlespawner({
					amount = 6,
					time = 0.25,
					minpos = {x = pos.x - 7/16, y = pos.y - 5/16, z = pos.z - 7/16},
					maxpos = {x = pos.x + 7/16, y = pos.y - 5/16, z = pos.z + 7/16},
					minvel = vector.new(-1, 2, -1),
					maxvel = vector.new(1, 5, 1),
					minacc = vector.new(0, -9.81, 0),
					maxacc = vector.new(0, -9.81, 0),
					collisiondetection = true,
					texture = "animalia_egg_fragment.png",
				})
                for i = 1, self.birth_count or 1 do
                    local object = minetest.add_entity(pos, self.name)
                    local ent = object:get_luaentity()
                    ent.growth_scale = 0.7
                    animalia.initialize_api(ent)
                    animalia.protect_from_despawn(ent)
                end
            end
            self.breeding = false
            self.breeding_cooldown = 300
            self:memorize("breeding", self.breeding)
            self:memorize("breeding_time", self.breeding_time)
            self:memorize("breeding_cooldown", self.breeding_cooldown)
            local minp = vector.subtract(pos, 1)
            local maxp = vec_add(pos, 1)
            animalia.particle_spawner(pos, "heart.png", "float", minp, maxp)
            return true
        end
        if not self:get_action() then
            creatura.action_walk(self, tpos)
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:attack", function(self, target, group)
    local punch_init = false
    if group then
        local allies = creatura.get_nearby_entities(self, self.name)
        if #allies > 0 then
            for i = 1, #allies do
                allies[i]:get_luaentity():initiate_utility("animalia:attack", allies[i]:get_luaentity(), target)
                allies[i]:get_luaentity():set_utility_score(1)
            end
        end
    end
    local function func(self)
        local target_alive, line_of_sight, tpos = self:get_target(target)
        if not target_alive then
            return true
        end
        local pos = self.object:get_pos()
        local dist = vec_dist(pos, tpos)
        if not self:get_action() then
            if punch_init then return true end
            --if dist > self:get_hitbox(self)[4] then
                creatura.action_walk(self, tpos, 6, "creatura:theta_pathfind", 1)
            --end
        end
        if dist <= self:get_hitbox(self)[4] + 1
        and not punch_init then
            animalia.action_punch(self, target)
            punch_init = true
        end
    end
    self:set_utility(func)
end)

-- Flight

creatura.register_utility("animalia:aerial_flock", function(self, scale)
    local range = ceil(8 * scale)
    local function func(self)
        if self:timer(2)
        and self.stamina <= 0 then
            local boids = creatura.get_boid_members(self.object:get_pos(), 6, self.name)
            if #boids > 1 then
                for i = 1, #boids do
                    local boid = boids[i]
                    local ent = boid:get_luaentity()
                    ent.stamina = ent:memorize("stamina", 0)
                    ent.is_landed = ent:memorize("is_landed", true)
                end
            end
        end
        local dist2floor = creatura.sensor_floor(self, 2, true)
        local dist2ceil = creatura.sensor_ceil(self, 2, true)
        if self.in_liquid then
            dist2floor = 0
            dist2ceil = 2
        end
        if dist2floor < 2
        and dist2ceil < 2 then
            self.is_landed = true
            return true
        end
        if not self:get_action()
        or (dist2floor < 2
        or dist2ceil < 2) then
            local pos = self.object:get_pos()
            local pos2 = self:get_wander_pos_3d(2, range)
            if dist2ceil < 2 then
                pos2.y = pos.y - 1
            end
            if dist2floor < 2 then
                pos2.y = pos.y + 1
            end
            if self.in_liquid then
                pos2.y = pos.y + 2
            end
            animalia.action_boid_move(self, pos2, 2)
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:aerial_swarm", function(self, scale)
    local function func(self)
        if self:timer(2)
        and self.stamina <= 0 then
            local boids = creatura.get_boid_members(self.object:get_pos(), 6, self.name)
            if #boids > 1 then
                for i = 1, #boids do
                    local boid = boids[i]
                    local ent = boid:get_luaentity()
                    ent.stamina = ent:memorize("stamina", 0)
                    ent.is_landed = ent:memorize("is_landed", true)
                end
            end
        end
        local dist2floor = creatura.sensor_floor(self, 2, true)
        local dist2ceil = creatura.sensor_ceil(self, 2, true)
        if self.in_liquid then
            dist2floor = 0
            dist2ceil = 2
        end
        if not self:get_action()
        or (dist2floor < 2
        or dist2ceil < 2) then
            local pos = self.object:get_pos()
            local pos2 = self:get_wander_pos_3d(1, 3)
            if dist2floor < 2 then
                pos2.y = pos.y + 1
            end
            if dist2ceil < 2 then
                pos2.y = pos.y - 1
            end
            animalia.action_boid_move(self, pos2, 2)
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:land", function(self, scale)
    local function func(self)
        if self.touching_ground then return true end
        local _, node = creatura.sensor_floor(self, 3, true)
        if node and is_liquid[node.name] then self.is_landed = false return true end
        scale = scale or 1
        local width = self.width
        local pos = self.object:get_pos()
        local pos2
        if self:timer(1) then
            local offset = random(2 * scale, 3 * scale)
            if random(2) < 2 then
                offset = offset * -1
            end
            pos2 = {
                x = pos.x + offset,
                y = pos.y,
                z = pos.z + offset
            }
            pos2.y = pos2.y - (3 * scale)
        end
        if not self:get_action()
        and pos2 then
            self:animate("fly")
            creatura.action_walk(self, pos2, 2, "animalia:fly_path", 1)
        end
    end
    self:set_utility(func)
end)

-- Swimming

creatura.register_utility("animalia:schooling", function(self)
    local pos = self.object:get_pos()
    local water = minetest.find_nodes_in_area(vector.subtract(pos, 5), vector.add(pos, 5), {"group:water"})
    local function func(self)
        if not self:get_action() then
            if #water < 1 then return true end
            local iter = random(#water)
            local pos2 = water[iter]
            table.remove(water, iter)
            animalia.action_boid_move(self, pos2, 2, "animalia:swim_obstacle_avoidance")
        end
    end
    self:set_utility(func)
end)

-- Resist Fall

creatura.register_utility("animalia:resist_fall", function(self)
    local function func(self)
        if not self:get_action() then
            animalia.action_fall(self)
        end
        if self.touching_ground
        or self.in_liquid then
            creatura.action_idle(self, "stand")
            self:set_gravity(-9.8)
            return true
        end
    end
    self:set_utility(func)
end)

-- Die

creatura.register_utility("animalia:die", function(self)
	local timer = 1.5
	local init = false
	local function func(self)
		if not init then
            self:play_sound("death")
			creatura.action_fallover(self)
			init = true
		end
		timer = timer - self.dtime
		if timer <= 0 then
            local pos = self.object:get_pos()
            minetest.add_particlespawner({
                amount = 8,
                time = 0.25,
                minpos = {x = pos.x - 0.1, y = pos.y, z = pos.z - 0.1},
                maxpos = {x = pos.x + 0.1, y = pos.y + 0.1, z = pos.z + 0.1},
                minacc = {x = 0, y = 2, z = 0},
                maxacc = {x = 0, y = 3, z = 0},
                minvel = {x = random(-1, 1), y = -0.25, z = random(-1, 1)},
                maxvel = {x = random(-2, 2), y = -0.25, z = random(-2, 2)},
                minexptime = 0.75,
                maxexptime = 1,
                minsize = 4,
                maxsize = 4,
                texture = "creatura_smoke_particle.png",
                animation = {
                    type = 'vertical_frames',
                    aspect_w = 4,
                    aspect_h = 4,
                    length = 1,
                },
                glow = 1
            })
            creatura.drop_items(self)
            self.object:remove()
        end
	end
    self:set_utility(func)
end)

-- Cat Exclusive Behaviors

creatura.register_utility("animalia:find_and_break_glass_vessels", function(self)
    local timer = 12
    local pos = self.object:get_pos()
    local pos2 = nil
    local nodes = minetest.find_nodes_in_area(
        vector.subtract(pos, 8),
        vec_add(pos, 8),
        {"vessels:glass_bottle", "vessels:drinking_glass"}
    )
    if #nodes > 0 then
        pos2 = nodes[1]
    end
    local func = function(self)
        if not pos2 then
            return
        end
        pos = self.object:get_pos()
        if not self:get_action() then
            creatura.action_walk(self, pos2, 6, "pathfind")
        end
        if vector.distance(pos, pos2) <= 0.5 then
            creatura.action_idle(self, 0.7, "smack")
            minetest.remove_node(pos2)
            minetest.add_item(pos2, "vessels:glass_fragments")
            if minetest.get_node(pos2).name == "air" then
                return true
            end
        end
        timer = timer - self.dtime
        if timer < 0 then return true end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:walk_ahead_of_player", function(self, player)
    if not player then return end
    local timer = 8
    local func = function(self)
		if not creatura.is_alive(player) then
            return true
		end
        local pos = self.object:get_pos()
        local tpos = player:get_pos()
        local dir = player:get_look_dir()
        tpos.x = tpos.x + dir.x
        tpos.z = tpos.z + dir.z
        self.status = self:memorize("status", "following")
        local dist = vec_dist(pos, tpos)
        if dist > self.view_range then
            self.status = self:memorize("status", "")
            return true
        end
        if not self:get_action() then
            if vec_dist(pos, tpos) > self.width + 0.5 then
                creatura.action_walk(self, tpos, 6, "pathfind", 0.75)
            else
                creatura.action_idle(self, 0.1, "stand")
            end
        end
        timer = timer - self.dtime
        if timer < 0 then self.status = self:memorize("status", "") return true end
    end
    self:set_utility(func)
end)

-- Bat Exclusive Behaviors

creatura.register_utility("animalia:return_to_home", function(self)
    local init = false
    local tpos = nil
    local function func(self)
        if not self.home_position then return true end
        local pos = self.object:get_pos()
        local pos2 = self.home_position
        if not self:get_action() then
            creatura.action_walk(self, vec_raise(pos2, -1), 6, "animalia:fly_path", 1)
        end
        local dist = vec_dist(pos, pos2)
        if dist < 2 then
            if is_solid[minetest.get_node(vec_raise(pos, 1)).name] then
                creatura.action_idle(self, 1, "latch")
                self:set_gravity(9.8)
                self.object:set_velocity({x = 0, y = 0, z = 0})
            end
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:find_home", function(self)
    local init = false
    local tpos = nil
    local pos = self.object:get_pos()
    local range = self.tracking_range
    local ceiling = get_ceiling_positions(pos, range / 2)
    local iter = 1
    local function func(self)
        if not ceiling[1] then
            return true
        else
            iter = random(#ceiling)
        end
        pos = self.object:get_pos()
        if not self:get_action() then
            local pos2 = get_wander_pos_3d(self, range)
            local dist2floor = creatura.sensor_floor(self, 5, true)
            local dist2ceil = creatura.sensor_ceil(self, 5, true)
            if dist2floor < 4 then
                pos2.y = pos.y + 2
            elseif dist2ceil < 4 then
                pos2.y = pos.y - 1
            end
            animalia.action_boid_move(self, pos2, 2)
        end
        if ceiling[iter] then
            local pos2 = {
                x = ceiling[iter].x,
                y = ceiling[iter].y - 1,
                z = ceiling[iter].z
            }
            local line_of_sight = fast_ray_sight(pos, pos2)
            if line_of_sight then
                self.home_position = self:memorize("home_position", ceiling[iter])
                return true
            end
        end
        if self:timer(1) then
            iter = iter + 1
            if iter > #ceiling then
                return true
            end
        end
    end
    self:set_utility(func)
end)

-- Tamed Animal Orders

creatura.register_utility("animalia:sit", function(self)
    local function func(self)
        if self.order ~= "sit" then
            return true
        end
        if not self:get_action() then
            creatura.action_idle(self, 0.1, "sit")
        end
    end
    self:set_utility(func)
end)

creatura.register_utility("animalia:mount", function(self, player)
    local function func(self)
        if not creatura.is_alive(player) then
            return true
        end
        local anim = "stand"
        local control = player:get_player_control()
        local speed_factor = 0
        local vel = self.object:get_velocity()
        if control.up then
            speed_factor = 1
            if control.aux1 then
                speed_factor = 1.5
            end
        end
        if control.jump
        and self.touching_ground then
            self.object:add_velocity({
                x = 0,
                y = self.jump_power + (abs(self._movement_data.gravity) * 0.33),
                z = 0
            })
        elseif not self.touching_ground then
            speed_factor = speed_factor * 0.5
        end
        local total_speed = vector.length(vel)
        if total_speed > 0.2 then
            anim = "walk"
            if total_speed > self.speed then
                anim = "run"
            end
            if not self.touching_ground
            and not self.in_liquid
            and vel.y > 0 then
                anim = "rear_constant"
            end
        end
        self:turn_to(player:get_look_horizontal())
        self:set_forward_velocity(self.speed * speed_factor)
        self:animate(anim)
        if control.sneak
        or not self.rider then
            animalia.mount(self, player)
            return true
        end
    end
    self:set_utility(func)
end)