------------------
-- Mob Core API --
------------------
----- Ver 0.1 ----

----------
-- Math --
----------

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

local vec_dir = vector.direction
local vec_dist = vector.distance

local function dist_2d(pos1, pos2)
    local a = vector.new(pos1.x, 0, pos1.z)
    local b = vector.new(pos2.x, 0, pos2.z)
    return vec_dist(a, b)
end

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

local creative = minetest.settings:get_bool("creative_mode")

--------------------
-- Misc Functions --
--------------------

local function is_node_walkable(name)
    local def = minetest.registered_nodes[name]
    return def and def.walkable
end

local function is_node_liquid(name)
    local def = minetest.registered_nodes[name]
    return def and def.drawtype == "liquid"
end

function mob_core.get_name_proper(str)
    if str then
        if str:match(":") then str = str:split(":")[2] end
        return (string.gsub(" " .. str, "%W%l", string.upper):sub(2):gsub("_", " "))
    end
end

function mob_core.find_val(tbl, val)
    for _, v in ipairs(tbl) do if v == val then return true end end
    return false
end

-------------------------
-- Logic-Use Functions --
-------------------------

function mob_core.is_mobkit_mob(object)
    if type(object) == 'userdata' then object = object:get_luaentity() end
    if type(object) == 'table' then
        if (object.logic or object.brainfunc) then
            return true
        else
            return false
        end
    else
        return false
    end
end

function mob_core.is_object_nearby(self, name)
    for _, obj in ipairs(self.nearby_objects) do
        if obj and obj:get_luaentity() and obj:get_luaentity().name == name then
            return true, obj
        end
    end
    return false
end

function mob_core.check_shared_owner(self, object)
    if not mobkit.is_alive(object) then return false end
    if object:is_player() then return false end
    if type(object) == 'userdata' then object = object:get_luaentity() end
    if not self.tamed then return false end
    if self.owner and object.owner then
        if self.owner == object.owner then
            return true
        else
            return false
        end
    end
    return false
end

mob_core.shared_owner = mob_core.check_shared_owner -- Deprecated name support

function mob_core.follow_holding(self, player)
    local item = player:get_wielded_item()
    local t = type(self.follow)
    if t == "string" and item:get_name() == self.follow then
        return true
    elseif t == "table" then
        for no = 1, #self.follow do
            if self.follow[no] == item:get_name() then return true end
        end
    end
    return false
end

-----------------------
-- Mob Item Handling --
-----------------------

function mob_core.register_spawn_egg(mob, col1, col2, inventory_image)
    if col1 and col2 then
        local len1 = string.len(col1)
        local len2 = string.len(col2)
        if len1 == 6 then col1 = col1 .. "d9" end
        if len2 == 6 then col2 = col2 .. "d9" end
        local base =
            "mob_core_spawn_egg_base.png^(mob_core_spawn_egg_base.png^[colorize:#" ..
                col1 .. ")"
        local spots =
            "mob_core_spawn_egg_overlay.png^(mob_core_spawn_egg_overlay.png^[colorize:#" ..
                col2 .. ")"
        inventory_image = base .. "^" .. spots
    end
    minetest.register_craftitem(mob:split(":")[1] .. ":spawn_" ..
                                    mob:split(":")[2], {
        description = "Spawn " .. mob_core.get_name_proper(mob),
        inventory_image = inventory_image,
        stack_max = 99,
        on_place = function(itemstack, _, pointed_thing)
            local mobdef = minetest.registered_entities[mob]
            local spawn_offset = math.abs(mobdef.collisionbox[2])
            local pos = minetest.get_pointed_thing_position(pointed_thing, true)
            pos.y = pos.y + spawn_offset
            minetest.add_entity(pos, mob)
            if not creative then
                itemstack:take_item()
                return itemstack
            end
        end
    })
end

function mob_core.register_set(mob, background, mask)
    local invimg = background
    if mask then
        invimg = "mob_core_spawn_egg_base.png^(" .. invimg ..
                     "^[mask:mob_core_spawn_egg_overlay.png)"
    end
    if not minetest.registered_entities[mob] then return end
    -- register new spawn egg containing mob information
    minetest.register_craftitem(mob .. "_set", {
        description = mob_core.get_name_proper(mob) .. " (Captured)",
        inventory_image = invimg,
        groups = {not_in_creative_inventory = 1},
        stack_max = 1,
        on_place = function(itemstack, placer, pointed_thing)
            local pos = pointed_thing.above
            -- am I clicking on something with existing on_rightclick function?
            local under = minetest.get_node(pointed_thing.under)
            local node = minetest.registered_nodes[under.name]
            if node and node.on_rightclick then
                return node.on_rightclick(pointed_thing.under, under, placer,
                                          itemstack)
            end
            if pos and not minetest.is_protected(pos, placer:get_player_name()) then
                pos.y = pos.y + 1
                local staticdata = itemstack:get_meta():get_string("staticdata")
                minetest.add_entity(pos, mob, staticdata)
                itemstack:take_item()
            end
            return itemstack
        end
    })
end

-----------------------
-- Spatial Functions --
-----------------------

function mob_core.sensor_floor(self, range, water)
    water = water or false
    local pos = self.object:get_pos()
    local node = minetest.get_node(pos)
    local dist = 0
    while (not is_node_walkable(node.name)
    or (water and minetest.registered_nodes[node.name].drawtype ~= "liquid"))
    and abs(dist) <= range do
        pos.y = pos.y - 1
        node = minetest.get_node(pos)
        dist = dist + 1
        if is_node_walkable(node.name)
        or (water and minetest.registered_nodes[node.name].drawtype == "liquid") then
            break
        end
    end
    if is_node_walkable(node.name)
    or (water and minetest.registered_nodes[node.name].drawtype == "liquid") then
        return dist
    elseif dist >= range then
        return range
    end
end

function mob_core.is_moveable(pos, width, height)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y, pos.z + width)
    for x = pos1.x, pos2.x do
        for z = pos1.z, pos2.z do
            local pos3 = vector.new(x, (pos.y + height), z)
            local pos4 = {x = pos3.x, y = pos.y + 1, z = pos3.z}
            local ray = minetest.raycast(pos3, pos4, false, false)
            for pointed_thing in ray do
                if pointed_thing.type == "node" then
                    return false
                end
            end
        end
    end
    return true
end

------------
-- Sounds --
------------

function mob_core.make_sound(self, sound)
    local spec = self.sounds and self.sounds[sound]
    local parameters = {object = self.object}

    if type(spec) == 'table' then
        if #spec > 0 then spec = spec[random(#spec)] end

        local function in_range(value)
            return type(value) == 'table' and value[1] + random() *
                       (value[2] - value[1]) or value
        end

        local pitch = 1.0

        pitch = pitch - (random(-10, 10) * 0.005)

        if self.child and self.sounds.alter_child_pitch then
            parameters.pitch = 2.0
        end

        minetest.sound_play(spec, parameters)

        if not spec.gain then spec.gain = 1.0 end
        if not spec.distance then spec.distance = 16 end

        -- pick random values within a range if they're a table
        parameters.gain = in_range(spec.gain)
        parameters.max_hear_distance = in_range(spec.distance)
        parameters.fade = in_range(spec.fade)
        parameters.pitch = pitch
        return minetest.sound_play(spec.name, parameters)
    end
    return minetest.sound_play(spec, parameters)
end

function mob_core.random_sound(self, chance) -- Random Sound
    if not chance then chance = 150 end
    if math.random(1, chance) == 1 then mobkit.make_sound(self, "random") end
end

----------------
-- Drop Items --
----------------

function mob_core.item_drop(self) -- Drop Items
    if not self.drops or #self.drops == 0 then return end
    local obj, item, num
    local pos = mobkit.get_stand_pos(self)
    for n = 1, #self.drops do
        if math.random(1, self.drops[n].chance) == 1 then
            num = math.random(self.drops[n].min or 0, self.drops[n].max or 1)
            item = self.drops[n].name
            if self.drops[n].min ~= 0 then
                obj = minetest.add_item(pos, ItemStack(item .. " " .. num))
                if obj then
                    local v = math.random(-1, 1)
                    obj:add_velocity({x = v, y = 1, z = v})
                end
            elseif obj then
                obj:remove()
            end
        end
    end
    self.drops = {}
end

------------------------
--  Damage and Vitals --
------------------------

-- Damage Indication

local function flash_red(self)
    minetest.after(0.0, function()
        self.object:settexturemod("^[colorize:#FF000040")
        core.after(0.2, function()
            if mobkit.is_alive(self) then
                self.object:settexturemod("")
            end
        end)
    end)
end

-- Death

function mob_core.on_die(self)
    local pos = mobkit.get_stand_pos(self)
    if self.driver then mob_core.force_detach(self.driver) end
    if self.owner then self.owner = nil end
    if self.sounds and self.sounds["death"] then
        mob_core.make_sound(self, "death")
    end
    self.object:set_velocity({x = 0, y = 0, z = 0})
    self.object:settexturemod("^[colorize:#FF000040")
    local timer = 1
    local start = true
    local func = function()
        if not mobkit.exists(self) then return true end
        if start then
            if self.animation and self.animation["death"] then
                mobkit.animate(self, "death")
            else
                mobkit.clear_queue_low(self)
                mobkit.lq_fallover(self)
            end
            self.logic = function() end -- brain dead as well
            start = false
        end
        timer = timer - self.dtime
        if timer <= 0 and mobkit.is_queue_empty_low(self) then
            if self.driver then mob_core.force_detach(self.driver) end
            mob_core.item_drop(self)
            minetest.add_particlespawner(
                {
                    amount = 12,
                    time = 0.1,
                    minpos = {
                        x = pos.x - self.collisionbox[4] * 0.75,
                        y = pos.y,
                        z = pos.z - self.collisionbox[4] * 0.75
                    },
                    maxpos = {
                        x = pos.x + self.collisionbox[4] * 0.75,
                        y = pos.y + self.collisionbox[4] * 0.75,
                        z = pos.z + self.collisionbox[4] * 0.75
                    },
                    minvel = {x = -0.2, y = -0.1, z = -0.2},
                    maxvel = {x = 0.2, y = -0.1, z = 0.2},
                    minacc = {x = 0, y = 0.25, z = 0},
                    maxacc = {x = 0, y = 0.45, z = 0},
                    minexptime = 1.5,
                    maxexptime = 2,
                    minsize = 2,
                    maxsize = 3,
                    collisiondetection = true,
                    vertical = false,
                    texture = "mob_core_red_particle.png"
                })
            self.object:remove()
        end
        minetest.after(2, function() -- fail safe
            if not mobkit.exists(self) then return true end
            if self.driver then mob_core.force_detach(self.driver) end
            mob_core.item_drop(self)
            minetest.add_particlespawner(
                {
                    amount = self.collisionbox[4] * 4,
                    time = 0.25,
                    minpos = {
                        x = pos.x - self.collisionbox[4] * 0.5,
                        y = pos.y,
                        z = pos.z - self.collisionbox[4] * 0.5
                    },
                    maxpos = {
                        x = pos.x + self.collisionbox[4] * 0.5,
                        y = pos.y + self.collisionbox[4] * 0.5,
                        z = pos.z + self.collisionbox[4] * 0.5
                    },
                    minacc = {x = -0.25, y = 0.5, z = -0.25},
                    maxacc = {x = 0.25, y = 0.25, z = 0.25},
                    minexptime = 0.75,
                    maxexptime = 1,
                    minsize = 4,
                    maxsize = 4,
                    texture = "mob_core_red_particle.png",
                    glow = 16
                })
            self.object:remove()
        end)
    end
    mobkit.queue_high(self, func, 100)
end

-- Vitals

function mob_core.vitals(self)
    if not mobkit.is_alive(self) then return end
    -- Fall Damage
    if self.fall_damage == nil then self.fall_damage = true end
    if self.fall_damage then
        if not self.isonground and not self.isinliquid and not self.fall_start then
            self.fall_start = mobkit.get_stand_pos(self).y
        end
        if self.fall_start then
            local fall_distance = self.fall_start - mobkit.get_stand_pos(self).y
            if not self.max_fall then self.max_fall = 3 end
            if self.isonground then
                if fall_distance > self.max_fall then
                    flash_red(self)
                    mob_core.make_sound(self, "hurt")
                    mobkit.hurt(self, fall_distance)
                end
                self.fall_start = nil
            end
        end
    end

    if mobkit.timer(self, 1) then
        -- Lava/Fire Damage
        if self.igniter_damage == nil then self.igniter_damage = true end

        if self.igniter_damage then
            local pos = mobkit.get_stand_pos(self)
            local node = minetest.get_node(pos)
            if node and minetest.registered_nodes[node.name].groups.igniter then
                if mobkit.timer(self, 1) then
                    flash_red(self)
                    mob_core.make_sound(self, "hurt")
                    mobkit.hurt(self, self.max_hp / 16)
                end
            end
        end

        -- Drowning
        if self.lung_capacity then
            local colbox = self.object:get_properties().collisionbox
            local headnode = mobkit.nodeatpos(
                                 mobkit.pos_shift(self.object:get_pos(),
                                                  {y = colbox[5]})) -- node at hitbox top
            if headnode and headnode.drawtype == 'liquid' then
                self.oxygen = self.oxygen - self.dtime
            else
                self.oxygen = self.lung_capacity
            end

            if self.oxygen <= 0 then
                if mobkit.timer(self, 2) then
                    flash_red(self)
                    mobkit.hurt(self, self.max_hp / self.lung_capacity)
                end
            end
        end
    end
end

-- Basic Damage

function mob_core.on_punch_basic(self, puncher, tool_capabilities, dir)
    local item = puncher:get_wielded_item()
    if mobkit.is_alive(self) then
        if self.immune_to then
            for i = 1, #self.immune_to do
                if item:get_name() == self.immune_to[i] then
                    return
                end
            end
        end
        if self.protected == true and puncher:get_player_name() ~= self.owner then
            return
        else
            flash_red(self)
            if self.isonground then
                local hvel = vector.multiply(
                                 vector.normalize({x = dir.x, y = 0, z = dir.z}),
                                 4)
                self.object:add_velocity({x = hvel.x, y = 2, z = hvel.z})
            end
            mobkit.hurt(self, tool_capabilities.damage_groups.fleshy or 1)
            if math.random(4) < 2 then
                mob_core.make_sound(self, "hurt")
            end
        end
    end
end

-- Retaliate

function mob_core.on_punch_retaliate(self, puncher, water, group)
    if mobkit.is_alive(self) then
        local pos = self.object:get_pos()
        if (not water) or (water and self.semiaquatic) then
            mob_core.hq_hunt(self, 10, puncher)
            if group then
                local objs = minetest.get_objects_inside_radius(pos,
                                                                self.view_range)
                for n = 1, #objs do
                    local luaent = objs[n]:get_luaentity()
                    if luaent and luaent.name == self.name and luaent.owner ==
                        self.owner and mobkit.is_alive(luaent) then
                        mob_core.hq_hunt(luaent, 10, puncher)
                    end
                end
            end
        elseif water and self.isinliquid then
            mob_core.hq_aqua_attack(self, 10, puncher, 1)
            if group then
                local objs = minetest.get_objects_inside_radius(pos,
                                                                self.view_range)
                for n = 1, #objs do
                    local luaent = objs[n]:get_luaentity()
                    if luaent and luaent.name == self.name and luaent.owner ==
                        self.owner and mobkit.is_alive(luaent) then
                        mob_core.hq_aqua_attack(luaent, 10, puncher, 1)
                    end
                end
            end
        end
    end
end

-- Runaway

function mob_core.on_punch_runaway(self, puncher, water, group)
    if mobkit.is_alive(self) then
        local pos = self.object:get_pos()
        if (not water) or (water and not self.isinliquid) then
            mobkit.hq_runfrom(self, 10, puncher)
            if group then
                local objs = minetest.get_objects_inside_radius(pos,
                                                                self.view_range)
                for n = 1, #objs do
                    local luaent = objs[n]:get_luaentity()
                    if luaent and luaent.name == self.name and luaent.owner ==
                        self.owner and mobkit.is_alive(luaent) then
                        mobkit.hq_runfrom(self, 10, puncher)
                    end
                end
            end
        elseif water and self.isinliquid then
            mob_core.hq_swimfrom(self, 10, puncher, 1)
            if group then
                local objs = minetest.get_objects_inside_radius(pos,
                                                                self.view_range)
                for n = 1, #objs do
                    local luaent = objs[n]:get_luaentity()
                    if luaent and luaent.name == self.name and luaent.owner ==
                        self.owner and mobkit.is_alive(luaent) then
                        mob_core.hq_swimfrom(luaent, 10, puncher, 1)
                    end
                end
            end
        end
    end
end

-- Default On Punch
-- This function should only be used for reference.

function mob_core.on_punch(self, puncher, time_from_last_punch,
                           tool_capabilities, dir)
    mob_core.on_punch_basic(self, puncher, time_from_last_punch,
                            tool_capabilities, dir)
end

-----------------
-- On Activate --
-----------------

local function set_gender(self)
    self.gender = mobkit.recall(self, "gender") or nil
    if not self.gender then
        if math.random(1, 2) == 1 then
            self.gender = mobkit.remember(self, "gender", "female")
        else
            self.gender = mobkit.remember(self, "gender", "male")
        end
    end
end

function mob_core.activate_nametag(self)
    if not self.nametag then return end
    self.object:set_properties({
        nametag = self.nametag,
        nametag_color = "#FFFFFF"
    })
end

function mob_core.set_textures(self)
    if not self.texture_no then
        if self.gender == "male" and self.male_textures then
            if #self.male_textures > 1 then
                self.texture_no = random(#self.male_textures)
            else
                self.texture_no = 1
            end
        end
        if self.gender == "female" and self.female_textures then
            if #self.female_textures > 1 then
                self.texture_no = random(#self.female_textures)
            else
                self.texture_no = 1
            end
        end
    end
    if self.textures and self.texture_no then
        local texture_no = self.texture_no
        local props = {}
        if self.gender == "female" then
            if self.female_textures then
                props.textures = {self.female_textures[texture_no]}
            else
                props.textures = {self.textures[texture_no]}
            end
        elseif self.gender == "male" then
            if self.male_textures then
                props.textures = {self.male_textures[texture_no]}
            else
                props.textures = {self.textures[texture_no]}
            end
        end
        if self.child and self.child_textures then
            if self.gender == "female" then
                if self.child_female_textures then
                    if texture_no > #self.child_female_textures then
                        texture_no = 1
                    end
                    props.textures = {self.child_female_textures[texture_no]}
                else
                    if texture_no > #self.child_textures then
                        texture_no = 1
                    end
                    props.textures = {self.child_textures[texture_no]}
                end
            elseif self.gender == "male" then
                if self.child_male_textures then
                    if texture_no > #self.child_male_textures then
                        texture_no = 1
                    end
                    props.textures = {self.child_male_textures[texture_no]}
                else
                    if texture_no > #self.child_textures then
                        texture_no = 1
                    end
                    props.textures = {self.child_textures[texture_no]}
                end
            end
        end
        self.object:set_properties(props)
    end
end

function mob_core.on_activate(self, staticdata, dtime_s) -- On Activate
    local init_props = {}
    if not self.textures then
        if self.female_textures then
            init_props.textures = {self.female_textures[1]}
        elseif self.male_textures then
            init_props.textures = {self.male_textures[1]}
        end
        self.object:set_properties(init_props)
    end
    if not self.textures then self.textures = {} end
    mobkit.actfunc(self, staticdata, dtime_s)
    self.tamed = mobkit.recall(self, "tamed") or false
    self.owner = mobkit.recall(self, "owner") or nil
    self.protected = mobkit.recall(self, "protected") or false
    self.food = mobkit.recall(self, "food") or 0
    self.punch_timer = 0
    self.growth_stage = mobkit.recall(self, "growth_stage") or 4
    self.growth_timer = mobkit.recall(self, "growth_timer") or 1801
    self.child = mobkit.recall(self, "child") or false
    self.status = mobkit.recall(self, "status") or ""
    self.nametag = mobkit.recall(self, "nametag") or ""
    self._tyaw = self.object:get_yaw()
    set_gender(self)
    mob_core.activate_nametag(self)
    mob_core.set_textures(self)
    if self.protected then self.timeout = nil end
    if self.growth_stage == 1 and self.scale_stage1 then
        mob_core.set_scale(self, self.scale_stage1)
    elseif self.growth_stage == 2 and self.scale_stage2 then
        mob_core.set_scale(self, self.scale_stage2)
    elseif self.growth_stage == 3 and self.scale_stage3 then
        mob_core.set_scale(self, self.scale_stage3)
    elseif self.growth_stage == 4 then
        mob_core.set_scale(self, 1)
    end
    local neighbor_offset = math.ceil(mob_core.get_hitbox(self)[4])
    self._neighbors = {
        {x = neighbor_offset, z = 0},
        {x = neighbor_offset, z = neighbor_offset},
        {x = 0, z = neighbor_offset},
        {x = -neighbor_offset, z = neighbor_offset},
        {x = -neighbor_offset, z = 0},
        {x = -neighbor_offset, z = -neighbor_offset},
        {x = 0, z = -neighbor_offset},
        {x = neighbor_offset, z = -neighbor_offset}
    }
end

-----------------------
-- Utility Functions --
-----------------------

-- Set Scale --

function mob_core.set_scale(self, scale)
    self.base_size = self.visual_size or {x = 1, y = 1, z = 1}
    self.base_colbox = self.collisionbox or {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}

    self.object:set_properties({
        visual_size = {
            x = self.base_size.x * scale,
            y = self.base_size.y * scale
        },
        collisionbox = {
            self.base_colbox[1] * scale, self.base_colbox[2] * scale,
            self.base_colbox[3] * scale, self.base_colbox[4] * scale,
            self.base_colbox[5] * scale, self.base_colbox[6] * scale
        }
    })
end

-- Set Owner --

function mob_core.set_owner(self, name)
    self.tamed = mobkit.remember(self, "tamed", true)
    self.owner = mobkit.remember(self, "owner", name)
end

--------------
-- Spawning --
--------------

-- Spawn Child Mob --

function mob_core.spawn_child(pos, mob)
    local obj = minetest.add_entity(pos, mob)
    local luaent = obj:get_luaentity()
    luaent.child = mobkit.remember(luaent, "child", true)
    luaent.growth_timer = mobkit.remember(luaent, "growth_timer", 1)
    luaent.growth_stage = mobkit.remember(luaent, "growth_stage", 1)
    mob_core.set_scale(luaent, luaent.scale_stage1 or 0.25)
    mob_core.set_textures(luaent)
    return obj
end

-- Spawning --

function mob_core.get_biome_name(pos)
    if not pos then return end
    return minetest.get_biome_name(minetest.get_biome_data(pos).biome)
end

local find_node_height = 32

local block_protected_spawn = minetest.settings:get_bool("block_protected_spawn") or true
local mob_limit = tonumber(minetest.settings:get("mob_limit")) or 6

function mob_core.spawn(name, nodes, min_light, max_light, min_height,
                        max_height, min_rad, max_rad, group, optional)
    group = group or 1
    local total_mobs = {}
    local mob_spawned = false
    local spawned_pos = nil
    if minetest.registered_entities[name] then
        for _, player in ipairs(minetest.get_connected_players()) do
            local spawn_mob = true
            local mobs_amount = 0
            for _, entity in pairs(minetest.luaentities) do
                if entity.name == name then
                    local ent_pos = entity.object:get_pos()
                    if ent_pos
                    and vec_dist(player:get_pos(), ent_pos) <= 1024 then
                        mobs_amount = mobs_amount + 1
                    end
                end
            end

            if mobs_amount >= mob_limit then spawn_mob = false end

            local int = {-1, 1}
            local pos = vector.floor(vector.add(player:get_pos(), 0.5))

            local x0, z0

            -- this is used to determine the axis buffer from the player
            local axis = math.random(0, 1)

            -- cast towards the direction
            if axis == 0 then -- x
                x0 = pos.x + math.random(min_rad, max_rad) * int[random(1, 2)]
                z0 = pos.z + math.random(-max_rad, max_rad)
            else -- z
                z0 = pos.z + math.random(min_rad, max_rad) * int[random(1, 2)]
                x0 = pos.x + math.random(-max_rad, max_rad)
            end

            local pos1 = vector.new(x0 - 5, pos.y - find_node_height, z0 - 5)
            local pos2 = vector.new(x0 + 5, pos.y + find_node_height, z0 + 5)
            local vm = minetest.get_voxel_manip()
            local emin, emax = vm:read_from_map(pos1, pos2)
            local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
            local data = vm:get_data()

            local spawner = {}

            for z = pos1.z, pos2.z do
                for y = pos1.y, pos2.y do
                    for x = pos1.x, pos2.x do
                        local vi = area:index(x, y, z)
                        local vi_pos = area:position(vi)
                        local vi_name = minetest.get_name_from_content_id(data[vi])
                        if is_node_walkable(minetest.get_name_from_content_id(data[vi])) then
                            local _vi = area:index(x, y + 1, z)
                            if data[_vi] == minetest.get_content_id("air") then
                                table.insert(spawner, area:position(_vi))
                            end
                        end
                    end
                end
            end

            if table.getn(spawner) > 0 then

                local mob_pos = spawner[1]

                if block_protected_spawn and minetest.is_protected(mob_pos, "") then
                    spawn_mob = false
                end

                if optional then
                    if optional.biomes then
                        if not mob_core.find_val(optional.biomes, mob_core.get_biome_name(pos)) then
                            spawn_mob = false
                        end
                    end
                end

                if mob_pos.y > max_height or mob_pos.y < min_height then
                    spawn_mob = false
                end

                local light = minetest.get_node_light(mob_pos)
                if not light or light > max_light or light < min_light then
                    spawn_mob = false
                end


                if spawn_mob then

                    mob_spawned = true

                    spawned_pos = mob_pos
    
                    local obj = minetest.add_entity(mob_pos, name)
    
                    table.insert(total_mobs, obj)
    
                    if group then
    
                        local attempts = 0
    
                        while attempts < group do
                            local mobdef = minetest.registered_entities[name]
                            local side = mobdef.collisionbox[4]
                            local group_pos =
                                vector.new(mob_pos.x +
                                               (random(-group, group) * side),
                                           mob_pos.y, mob_pos.z +
                                               (random(-group, group) * side))
                            local spawn_pos =
                                minetest.find_nodes_in_area_under_air(
                                    vector.new(group_pos.x, group_pos.y - 8,
                                               group_pos.z),
                                    vector.new(group_pos.x, group_pos.y + 8,
                                               group_pos.z), nodes)
                            if spawn_pos[1] then
                                local obj_i =
                                    minetest.add_entity(
                                        vector.new(spawn_pos[1].x, spawn_pos[1].y +
                                                       math.abs(
                                                           mobdef.collisionbox[2]),
                                                   spawn_pos[1].z), name)
                                table.insert(total_mobs, obj_i)
                            end
                            attempts = attempts + 1
                        end
                    end
                    if mob_core.registered_on_spawns[name] then
                        mob_core.registered_spawns[name].last_pos = spawned_pos
                        mob_core.registered_spawns[name].mobs = total_mobs
                        local on_spawn = mob_core.registered_on_spawns[name]
                        on_spawn.func(unpack(on_spawn.args))
                    end
                end
            end
        end
        return mob_spawned, spawned_pos, total_mobs
    end
end

function mob_core.force_spawn(pos, mob)
    minetest.forceload_block(pos, false)
    minetest.after(4, function()
        local ent = minetest.add_entity(pos, mob)
        minetest.after(0.01, function()
            local loop = true
            local objects = minetest.get_objects_inside_radius(pos, 0.5)
            for i = 1, #objects do
                local object = objects[i]
                if object
                and object:get_luaentity()
                and object:get_luaentity().name == mob then
                    loop = false
                end
            end
            minetest.after(1, function()
                minetest.forceload_free_block(pos)
            end)
            if loop then
                mob_core.force_spawn(pos, mob)
            end 
        end)
    end)
end

function mob_core.spawn_at_pos(pos, name, nodes, group, optional)
    group = group or 1
    if minetest.registered_entities[name] then
        local spawn_mob = true
        local mobs_amount = 0
        for _, entity in pairs(minetest.luaentities) do
            if entity.name == name then
                local ent_pos = entity.object:get_pos()
                if ent_pos
                and vec_dist(pos, ent_pos) <= 1024 then
                    mobs_amount = mobs_amount + 1
                end
            end
        end

        if mobs_amount >= mob_limit then spawn_mob = false end

        local int = {-1, 1}

        local pos1 = vector.new(pos.x - 5, pos.y - 5, pos.z - 5)
        local pos2 = vector.new(pos.x + 5, pos.y + 5, pos.z + 5)

        local spawner = minetest.find_nodes_in_area_under_air(pos1, pos2, mob_core.walkable_nodes)

        if table.getn(spawner) > 0 then

            local mob_pos = spawner[1]

            mob_pos.y = mob_pos.y + 1

            if block_protected_spawn and minetest.is_protected(mob_pos, "") then
                spawn_mob = false
            end

            if optional then
                if optional.biomes then
                    spawn_mob = false
                    local biome = mob_core.get_biome_name(mob_pos)
                    for i = 1, #optional.biomes do
                        if optional.biomes[i]:match("^" .. biome) then
                            spawn_mob = true
                        end
                    end
                end
            end

            if spawn_mob then

                mob_core.force_spawn(mob_pos, name)

                if group then

                    local attempts = 0

                    while attempts < group do
                        local mobdef = minetest.registered_entities[name]
                        local side = mobdef.collisionbox[4]
                        local group_pos =
                            vector.new(mob_pos.x +
                                           (random(-group, group) * side),
                                       mob_pos.y, mob_pos.z +
                                           (random(-group, group) * side))
                        local spawn_pos =
                            minetest.find_nodes_in_area_under_air(
                                vector.new(group_pos.x, group_pos.y - 8,
                                           group_pos.z),
                                vector.new(group_pos.x, group_pos.y + 8,
                                           group_pos.z), mob_core.walkable_nodes)
                        if spawn_pos[1] then
                            mob_core.force_spawn(
                                vector.new(spawn_pos[1].x, spawn_pos[1].y +
                                               math.abs(
                                                   mobdef.collisionbox[2]),
                                           spawn_pos[1].z), name)
                        end
                        attempts = attempts + 1
                    end
                end
            end
        end
    end
end

mob_core.registered_on_spawns = {}

mob_core.registered_spawns = {}

function mob_core.register_spawn(def, interval, chance)
    local spawn_timer = 0
    mob_core.registered_spawns[def.name] = {func = nil, last_pos = {}, def = def}
    mob_core.registered_spawns[def.name].func =
        minetest.register_globalstep(function(dtime)
            spawn_timer = spawn_timer + dtime
            if spawn_timer > interval then
                if random(1, chance) == 1 then
                    local spawned, last_pos, mobs =
                        mob_core.spawn(def.name, def.nodes or
                                           {"group:soil", "group:stone"},
                                       def.min_light or 0, def.max_light or 15,
                                       def.min_height or -31000,
                                       def.max_height or 31000,
                                       def.min_rad or 24, def.max_rad or 256,
                                       def.group or 1, def.optional or nil)
                end
                spawn_timer = 0
            end
        end)
end

function mob_core.register_on_spawn(name, func, ...)
    mob_core.registered_on_spawns[name] = {args = {...}, func = func}
end

-------------
-- On Step --
-------------

-- Push on entity collision --

function mob_core.collision_detection(self)
    if not mobkit.is_alive(self) then return end
    local pos = self.object:get_pos()
    local hitbox = mob_core.get_hitbox(self)
    local width = -hitbox[1] + hitbox[4] + 0.5
    local objects = minetest.get_objects_inside_radius(pos, width)
    if #objects < 2 then return end
    local is_in_bed = function(object)
        if not minetest.get_modpath("beds") then return false end
        if not beds.player
        or not object:is_player()
        or (object:is_player()
        and not beds.player[object:get_player_name()]) then
            return false
        end
        return true
    end
    local col_no = 0
    for i = 1, #objects do
        local object = objects[i]
        if (object and object ~= self.object) and
            (not object:get_attach() or object:get_attach() ~= self.object) and
            (not self.object:get_attach() or self.object:get_attach() ~= object) and
            (((object:get_luaentity() and object:get_luaentity().logic)) or
                (object:is_player() and not is_in_bed(object))) then
            col_no = col_no + 1
            if col_no >= 5 then break end
            local pos2 = object:get_pos()
            local dir = vec_dir(pos, pos2)
            dir.y = 0
            if dir.x == 0 and dir.z == 0 then
                dir = vector.new(random(-1, 1) * random(), 0,
                                 random(-1, 1) * random())
            end
            local velocity = vector.multiply(dir, 1.1)
            local vel1 = vector.multiply(velocity, -1)
            local vel2 = velocity
            self.object:add_velocity(vel1)
            object:add_velocity(vel2)
        end
    end
end

-- 4 Stage Growth --

function mob_core.growth(self, interval)
    if not mobkit.is_alive(self) then return end
    if self.growth_stage == 4 then return end
    if not self.base_hp then
        self.base_hp = mobkit.remember(self, "base_hp", self.max_hp)
    end
    local pos = self.object:get_pos()
    local reach = minetest.registered_entities[self.name].reach
    local speed = minetest.registered_entities[self.name].max_speed
    interval = interval or 400
    if not self.scale_stage1 then
        self.scale_stage1 = 0.25
        self.scale_stage2 = 0.5
        self.scale_stage3 = 0.75
    end
    if self.growth_stage < 4 then self.growth_timer = self.growth_timer + 1 end
    if self.growth_timer > interval then
        self.growth_timer = 1
        self.max_speed = speed / 4
        if reach then self.reach = reach / 4 end
        if self.growth_stage == 1 then
            self.growth_stage = mobkit.remember(self, "growth_stage", 2)
            self.object:set_pos({
                x = pos.x,
                y = pos.y + math.abs(self.collisionbox[2]),
                z = pos.z
            })
            mob_core.set_scale(self, self.scale_stage2)
            self.max_speed = speed / 2
            if reach then self.reach = reach / 2 end
        elseif self.growth_stage == 2 then
            self.growth_stage = mobkit.remember(self, "growth_stage", 3)
            self.object:set_pos({
                x = pos.x,
                y = pos.y + math.abs(self.collisionbox[2]),
                z = pos.z
            })
            mob_core.set_scale(self, self.scale_stage3)
            self.child = mobkit.remember(self, "child", false)
            mob_core.set_textures(self)
            self.max_speed = speed / 1.5
            if reach then self.reach = reach / 1.5 end
        elseif self.growth_stage == 3 then
            self.growth_stage = mobkit.remember(self, "growth_stage", 4)
            self.object:set_pos({
                x = pos.x,
                y = pos.y + math.abs(self.collisionbox[2]),
                z = pos.z
            })
            mob_core.set_scale(self, 1)
            self.max_speed = speed
            if reach then self.reach = reach end
        end
    end
    if self.growth_stage == 1 and self.hp > self.max_hp / 4 then
        self.hp = self.max_hp / 4
    end
    if self.growth_stage == 2 and self.hp > self.max_hp / 2 then
        self.hp = self.max_hp / 2
    end
    if self.growth_stage == 3 and self.hp > self.max_hp / 1.5 then
        self.hp = self.max_hp / 1.5
    end
    self.growth_timer = mobkit.remember(self, "growth_timer", self.growth_timer)
end

-- Step Function --

function mob_core.on_step(self, dtime, moveresult)
    mobkit.stepfunc(self, dtime, moveresult)
    if self.owner_target
    and not mobkit.exists(self.owner_target) then
        self.owner_target = nil
    end
    if self.push_on_collide then
        mob_core.collision_detection(self)
    end
end

--------------------------
-- Rightclick Functions --
--------------------------

-- Mount --

function mob_core.mount(self, clicker)
    if not self.driver and self.child == false then
        mobkit.clear_queue_high(self)
        self.status = mobkit.remember(self, "status", "ridden")
        mob_core.attach(self, clicker)
        return false
    else
        return true
    end
end

-- Capture Mob --

function mob_core.capture_mob(self, clicker, capture_tool, capture_chance, wear, force_take)
    if not clicker:is_player() or not clicker:get_inventory() then
        return false
    end
    local mobname = self.name
    local catcher = clicker:get_player_name()
    local tool = clicker:get_wielded_item()
    if tool:get_name() ~= capture_tool then return false end
    if self.tamed == false then
        minetest.chat_send_player(catcher, "Mob is not tamed.")
        return false
    end
    if self.owner ~= catcher and force_take == false then
        minetest.chat_send_player(catcher, "Mob is owned by @1" .. self.owner)
        return false
    end
    if clicker:get_inventory():room_for_item("main", mobname) then
        local chance = 0
        if tool:get_name() == capture_tool then
            if capture_tool == "" then
                chance = capture_chance
            else
                chance = capture_chance
                tool:add_wear(wear)
                clicker:set_wielded_item(tool)
            end
        end
        if chance and chance > 0 and random(1, 100) <= chance then
            local new_stack = ItemStack(mobname .. "_set")
            new_stack:get_meta():set_string("staticdata", self:get_staticdata())
            local inv = clicker:get_inventory()
            if inv:room_for_item("main", new_stack) then
                inv:add_item("main", new_stack)
            else
                minetest.add_item(clicker:get_pos(), new_stack)
            end
            self.object:remove()
            return new_stack
        elseif chance and chance ~= 0 then
            return false
        elseif not chance then
            return false
        end
    end

    return true
end

-- Feed/Tame/Breed --

function mob_core.feed_tame(self, clicker, feed_count, tame, breed)
    local item = clicker:get_wielded_item()
    local pos = self.object:get_pos()
    local mob_name = mob_core.get_name_proper(self.name)
    if mob_core.follow_holding(self, clicker) then
        if creative == false then
            item:take_item()
            clicker:set_wielded_item(item)
        end
        mobkit.heal(self, self.max_hp / feed_count)
        if self.hp >= self.max_hp then self.hp = self.max_hp end
        self.food = mobkit.remember(self, "food", self.food + 1)
        if self.food >= feed_count then
            self.food = mobkit.remember(self, "food", 0)
            if tame and not self.tamed then
                mob_core.set_owner(self, clicker:get_player_name())
                minetest.chat_send_player(clicker:get_player_name(),
                                          mob_name .. " has been tamed!")
                mobkit.clear_queue_high(self)
                minetest.add_particlespawner(
                    {
                        amount = 16,
                        time = 0.25,
                        minpos = {
                            x = pos.x - self.collisionbox[4],
                            y = pos.y - self.collisionbox[4],
                            z = pos.z - self.collisionbox[4]
                        },
                        maxpos = {
                            x = pos.x + self.collisionbox[4],
                            y = pos.y + self.collisionbox[4],
                            z = pos.z + self.collisionbox[4]
                        },
                        minacc = {x = 0, y = 0.25, z = 0},
                        maxacc = {x = 0, y = -0.25, z = 0},
                        minexptime = 0.75,
                        maxexptime = 1,
                        minsize = 4,
                        maxsize = 4,
                        texture = "mob_core_green_particle.png",
                        glow = 16
                    })
            end
            if breed then
                if self.child then return false end
                if self.breed_mode then return false end
                if self.breed_timer == 0 and self.breed_mode == false then
                    self.breed_mode = true
                    minetest.add_particlespawner(
                        {
                            amount = 16,
                            time = 0.25,
                            minpos = {
                                x = pos.x - self.collisionbox[4],
                                y = pos.y - self.collisionbox[4],
                                z = pos.z - self.collisionbox[4]
                            },
                            maxpos = {
                                x = pos.x + self.collisionbox[4],
                                y = pos.y + self.collisionbox[4],
                                z = pos.z + self.collisionbox[4]
                            },
                            minacc = {x = 0, y = 0.25, z = 0},
                            maxacc = {x = 0, y = -0.25, z = 0},
                            minexptime = 0.75,
                            maxexptime = 1,
                            minsize = 4,
                            maxsize = 4,
                            texture = "heart.png",
                            glow = 16
                        })
                end
            end
        end
    end
    return false
end

-- Protection --

function mob_core.protect(self, clicker, force_protect)
    local name = clicker:get_player_name()
    local item = clicker:get_wielded_item()
    local mob_name = mob_core.get_name_proper(self.name)
    if item:get_name() ~= "mob_core:protection_gem" then return false end
    if self.tamed == false and not force_protect then
        minetest.chat_send_player(name, mob_name .. " is not tamed")
        return true
    end
    if self.protected == true then
        minetest.chat_send_player(name, mob_name .. " is already protected")
        return true
    end
    if not creative then
        item:take_item()
        clicker:set_wielded_item(item)
    end
    self.protected = true
    mobkit.remember(self, "protected", self.protected)
    self.timeout = nil
    local pos = self.object:get_pos()
    pos.y = pos.y + self.collisionbox[2] + 1 / 1
    minetest.add_particlespawner({
        amount = 16,
        time = 0.25,
        minpos = {
            x = pos.x - self.collisionbox[4],
            y = pos.y - self.collisionbox[4],
            z = pos.z - self.collisionbox[4]
        },
        maxpos = {
            x = pos.x + self.collisionbox[4],
            y = pos.y + self.collisionbox[4],
            z = pos.z + self.collisionbox[4]
        },
        minacc = {x = 0, y = 0.25, z = 0},
        maxacc = {x = 0, y = -0.25, z = 0},
        minexptime = 0.75,
        maxexptime = 1,
        minsize = 4,
        maxsize = 4,
        texture = "mob_core_green_particle.png",
        glow = 16
    })
    return true
end

-- Nameing --

local nametag_obj = {}
local nametag_item = {}

function mob_core.nametag(self, clicker, force_name)
    if not force_name and clicker:get_player_name() ~= self.owner then return end
    local item = clicker:get_wielded_item()
    if item:get_name() == "mob_core:nametag" then

        local name = clicker:get_player_name()

        nametag_obj[name] = self
        nametag_item[name] = item

        local tag = self.nametag or ""

        minetest.show_formspec(name, "mob_core_nametag",
                               "size[8,4]" .. "field[0.5,1;7.5,0;name;" ..
                                   minetest.formspec_escape("Enter name:") ..
                                   ";" .. tag .. "]" ..
                                   "button_exit[2.5,3.5;3,1;mob_rename;" ..
                                   minetest.formspec_escape("Rename") .. "]")
    end
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
    if formname == "mob_core_nametag" and fields.name then

        local name = player:get_player_name()

        if not nametag_obj[name] or not nametag_obj[name].object then
            return
        end

        local item = player:get_wielded_item()

        if item:get_name() ~= "mob_core:nametag" then return end

        if string.len(fields.name) > 64 then
            fields.name = string.sub(fields.name, 1, 64)
        end

        nametag_obj[name].nametag = mobkit.remember(nametag_obj[name],
                                                    "nametag", fields.name)

        mob_core.activate_nametag(nametag_obj[name])

        if fields.name ~= "" and not creative then
            nametag_item[name]:take_item()
            player:set_wielded_item(nametag_item[name])
        end

        nametag_obj[name] = nil
        nametag_item[name] = nil
    end
end)

-----------------
-- Pathfinding --
-----------------

-- Lightweight Pathfinder

local function can_fit(pos, width, single_plane)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y, pos.z + width)
    for x = pos1.x, pos2.x do
        for y = pos1.y, pos2.y do
            for z = pos1.z, pos2.z do
                local p2 = vector.new(x, y, z)
                local node = minetest.get_node(p2)
                if is_node_walkable(node.name) then
                    local p3 = vector.new(p2.x, p2.y + 1, p2.z)
                    local node2 = minetest.get_node(p3)
                    if minetest.registered_nodes[node2.name].walkable then
                        return false
                    end
                    if single_plane then return false end
                end
            end
        end
    end
    return true
end

local function move_from_wall(pos, width)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y, pos.z + width)
    for x = pos1.x, pos2.x do
        for y = pos1.y, pos2.y do
            for z = pos1.z, pos2.z do
                local p2 = vector.new(x, y, z)
                if can_fit(p2, width) and vec_dist(pos, p2) < width then
                    return p2
                end
            end
        end
    end
    return pos
end

function mob_core.find_path_lite(pos, tpos, width)

    local raw

    if not minetest.registered_nodes[minetest.get_node(
        vector.new(pos.x, pos.y - 1, pos.z)).name].walkable then
        local min = vector.subtract(pos, width + 1)
        local max = vector.add(pos, width + 1)

        local index_table = minetest.find_nodes_in_area_under_air(min, max,
                                                                  mob_core.walkable_nodes)
        for _, i_pos in pairs(index_table) do
            if minetest.registered_nodes[minetest.get_node(i_pos).name].walkable then
                pos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z)
                break
            end
        end
    end

    if not minetest.registered_nodes[minetest.get_node(
        vector.new(tpos.x, tpos.y - 1, tpos.z)).name].walkable then
        local min = vector.subtract(tpos, width)
        local max = vector.add(tpos, width)

        local index_table = minetest.find_nodes_in_area_under_air(min, max,
                                                                  mob_core.walkable_nodes)
        for _, i_pos in pairs(index_table) do
            if minetest.registered_nodes[minetest.get_node(i_pos).name].walkable then
                tpos = vector.new(i_pos.x, i_pos.y + 1, i_pos.z)
                break
            end
        end
    end

    local path = minetest.find_path(pos, tpos, 32, 2, 2, "A*_noprefetch")

    if not path then return end

    table.remove(path, 1)

    for i = #path, 1, -1 do
        if not path then return end
        if vec_dist(pos, path[i]) <= width + 1 then
            for _i = 3, #path do path[_i - 1] = path[_i] end
        end

        if not can_fit(path[i], width + 1) then
            local clear = move_from_wall(path[i], width + 1)
            if clear and can_fit(clear, width) then path[i] = clear end
        end

        if minetest.get_node(path[i]).name == "default:snow" then
            path[i] = vector.new(path[i].x, path[i].y + 1, path[i].z)
        end

        raw = path
        if #path > 3 then

            if vec_dist(pos, path[i]) < width then
                table.remove(path, i)
            end

            local pos1 = path[i - 2]
            local pos2 = path[i]
            -- Handle Diagonals
            if pos1 and pos2 and pos1.x ~= pos2.x and pos1.z ~= pos2.z then
                if minetest.line_of_sight(pos1, pos2) then
                    if can_fit(pos, width) then
                        table.remove(path, i - 1)
                    end
                end
            end
            -- Reduce Straight Lines
            if pos1 and pos2 and pos1.x == pos2.x and pos1.z ~= pos2.z and
                pos1.y == pos2.y then
                if minetest.line_of_sight(pos1, pos2) then
                    if can_fit(pos, width) then
                        table.remove(path, i - 1)
                    end
                end
            elseif pos1 and pos2 and pos1.x ~= pos2.x and pos1.z == pos2.z and
                pos1.y == pos2.y then
                if minetest.line_of_sight(pos1, pos2) then
                    if can_fit(pos, width) then
                        table.remove(path, i - 1)
                    end
                end
            end
        end
    end

    return path, raw
end

-- A* Pathfinder with object scale and 3d movement support --

local moveable = mob_core.is_moveable

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_node_walkable(node_under.name) and not is_node_walkable(node.name)
    if walkable then
        return pos2
    elseif not walkable then
        if not is_node_walkable(node_under.name) then
            while not is_node_walkable(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_node_walkable(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

-- Get Distance

local function get_distance(start_pos, end_pos)
    local distX = abs(start_pos.x - end_pos.x)
    local distZ = abs(start_pos.z - end_pos.z)

    if distX > distZ then
        return 14 * distZ + 10 * (distX - distZ)
    else
        return 14 * distX + 10 * (distZ - distX)
    end
end

local function get_distance_to_neighbor(start_pos, end_pos)
    local distX = abs(start_pos.x - end_pos.x)
    local distY = abs(start_pos.y - end_pos.y)
    local distZ = abs(start_pos.z - end_pos.z)

    if distX > distZ then
        return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
    else
        return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
    end
end

-- Check if pos is above ground

local function is_on_ground(pos)
    local ground = {
        x = pos.x,
        y = pos.y - 1,
        z = pos.z
    }
    if is_node_walkable(minetest.get_node(ground).name) then
        return true
    end
    return false
end

-- Find a path from start to goal

function mob_core.find_path(start, goal, obj_width, obj_height, max_open, climb, fly, swim)
    climb = climb or false
    fly = fly or false
    swim = swim or false

    local path_neighbors = {
        {x = 1, y = 0, z = 0},
        {x = 1, y = 0, z = 1},
        {x = 0, y = 0, z = 1},
        {x = -1, y = 0, z = 1},
        {x = -1, y = 0, z = 0},
        {x = -1, y = 0, z = -1},
        {x = 0, y = 0, z = -1},
        {x = 1, y = 0, z = -1}
    }

    if climb then
        table.insert(path_neighbors, {x = 0, y = 1, z = 0})
    end

    if fly
    or swim then
        path_neighbors = {
            -- Central
            {x = 1, y = 0, z = 0},
            {x = 0, y = 0, z = 1},
            {x = -1, y = 0, z = 0},
            {x = 0, y = 0, z = -1},
            -- Up
            {x = 1, y = 1, z = 0},
            {x = 0, y = 1, z = 1},
            {x = -1, y = 1, z = 0},
            {x = 0, y = 1, z = -1},
            -- Down
            {x = 1, y = 1, z = 0},
            {x = 0, y = 1, z = 1},
            {x = -1, y = 1, z = 0},
            {x = 0, y = 1, z = -1},
            -- Directly Up or Down
            {x = 0, y = 1, z = 0},
            {x = 0, y = -1, z = 0}
        }
    end

    local function get_neighbors(pos, width, height, tbl, open, closed)
        local result = {}
        for i = 1, #tbl do
            local neighbor = vector.add(pos, tbl[i])
            if neighbor.y == pos.y
            and not fly
            and not swim then
                neighbor = get_ground_level(neighbor, 1)
            end
            local line_of_sight = minetest.line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
            if swim then
                line_of_sight = true
            end
            if (parent_pos
            and dist_2d(pos, neighbor) == dist_2d(parent_pos, neighbor))
            or open[minetest.hash_node_position(neighbor)]
            or closed[minetest.hash_node_position(neighbor)] then
                line_of_sight = false
            end
            if line_of_sight
            and moveable(neighbor, width, height)
            and ((is_on_ground(neighbor)
            or (fly or swim))
            or (neighbor.x == pos.x
            and neighbor.z == pos.z
            and climb))
            and (not swim
            or is_node_liquid(minetest.get_node(neighbor).name)) then
                table.insert(result, neighbor)
            end
        end
        return result
    end

    local function find_path(start, goal)

        start = {
            x = floor(start.x + 0.5),
            y = floor(start.y + 0.5),
            z = floor(start.z + 0.5)
        }
    
        goal = {
            x = floor(goal.x + 0.5),
            y = floor(goal.y + 0.5),
            z = floor(goal.z + 0.5)
        }

        if goal.x == start.x
        and goal.z == start.z then -- No path can be found
            return nil
        end
    
        local openSet = {}
    
        local closedSet = {}
    
        local start_index = minetest.hash_node_position(start)
    
        openSet[start_index] = {
            pos = start,
            parent = nil,
            gScore = 0,
            fScore = get_distance(start, goal)
        }
    
        local count = 1
    
        while count > 0 do
            -- Initialize ID and data
            local current_id
            local current
    
            -- Get an initial id in open set
            for i, v in pairs(openSet) do
                current_id = i
                current = v
                break
            end
    
            -- Find lowest f cost
            for i, v in pairs(openSet) do
                if v.fScore < current.fScore then
                    current_id = i
                    current = v
                end
            end
    
            -- Add lowest fScore to closedSet and remove from openSet
            openSet[current_id] = nil
            closedSet[current_id] = current
    
            -- Reconstruct path if end is reached
            if (is_on_ground(goal)
            and current_id == minetest.hash_node_position(goal))
            or (not is_on_ground(goal)
            and goal.x == current.pos.x
            and goal.z == current.pos.z) then
                local path = {}
                local fail_safe = 0
                for k, v in pairs(closedSet) do
                    fail_safe = fail_safe + 1
                end
                repeat
                    if not closedSet[current_id] then return end
                    table.insert(path, closedSet[current_id].pos)
                    current_id = closedSet[current_id].parent
                until current_id == start_index or #path >= fail_safe
                table.insert(path, closedSet[current_id].pos)
                local reverse_path = {}
                repeat table.insert(reverse_path, table.remove(path)) until #path == 0
                return reverse_path
            end
    
            count = count - 1
    
            local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet)
    
            -- Go through neighboring nodes
            for i = 1, #adjacent do
                local neighbor = {
                    pos = adjacent[i],
                    parent = current_id,
                    gScore = 0,
                    fScore = 0
                }
                temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
                local new_gScore = 0
                if openSet[minetest.hash_node_position(neighbor.pos)] then
                    new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
                end
                if (temp_gScore < new_gScore
                or not openSet[minetest.hash_node_position(neighbor.pos)])
                and not closedSet[minetest.hash_node_position(neighbor.pos)] then
                    if not openSet[minetest.hash_node_position(neighbor.pos)] then
                        count = count + 1
                    end
                    local hCost = get_distance_to_neighbor(neighbor.pos, goal)
                    neighbor.gScore = temp_gScore
                    neighbor.fScore = temp_gScore + hCost
                    openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
                end
            end
            if count > (max_open or 100) then return end
        end
        return nil
    end
    return find_path(start, goal)
end

---------------------------
-- Overwritten Functions --
---------------------------

local old_turn2yaw = mobkit.turn2yaw

function mobkit.turn2yaw(self, tyaw, rate)
    old_turn2yaw(self, tyaw, rate)
    self._tyaw = tyaw
end

-- Force Tame Command --

minetest.register_chatcommand("force_tame", {
    params = "",
    description = "tame pointed mobkit mob",
    privs = {server = true, creative = true},
    func = function(name)
        local player = minetest.get_player_by_name(name)
        if not player then return false end
        local dir = player:get_look_dir()
        local pos = player:get_pos()
        pos.y = pos.y + player:get_properties().eye_height or 1.625
        local dest = vector.add(pos, vector.multiply(dir, 40))
        local ray = minetest.raycast(pos, dest, true, false)
        for pointed_thing in ray do
            if pointed_thing.type == "object" then
                local pointedobject = pointed_thing.ref
                if pointedobject:get_luaentity() then
                    pointedobject = pointedobject:get_luaentity()
                    local mob_name =
                        mob_core.get_name_proper(pointedobject.name)
                    if not pointedobject.tamed then
                        if not pointedobject.logic or pointedobject.brainfunc then
                            minetest.chat_send_player(name,
                                                      "This command only works on mobkit mobs")
                            return
                        end
                        mob_core.set_owner(pointedobject, name)
                        minetest.chat_send_player(name, mob_name ..
                                                      " has been tamed!")
                        mobkit.clear_queue_high(pointedobject)
                        pos = pointedobject.object:get_pos()
                        minetest.add_particlespawner(
                            {
                                amount = 16,
                                time = 0.25,
                                minpos = {
                                    x = pos.x - pointedobject.collisionbox[4],
                                    y = pos.y - pointedobject.collisionbox[4],
                                    z = pos.z - pointedobject.collisionbox[4]
                                },
                                maxpos = {
                                    x = pos.x + pointedobject.collisionbox[4],
                                    y = pos.y + pointedobject.collisionbox[4],
                                    z = pos.z + pointedobject.collisionbox[4]
                                },
                                minacc = {x = 0, y = 0.25, z = 0},
                                maxacc = {x = 0, y = -0.25, z = 0},
                                minexptime = 0.75,
                                maxexptime = 1,
                                minsize = 4,
                                maxsize = 4,
                                texture = "mob_core_green_particle.png",
                                glow = 16
                            })
                        return
                    else
                        if not pointedobject.logic or pointedobject.brainfunc then
                            minetest.chat_send_player(name,
                                                      "This command only works on mobkit mobs")
                            return
                        end
                        mob_core.set_owner(pointedobject, name)
                        minetest.chat_send_player(name, mob_name ..
                                                      " has been tamed!")
                        mobkit.clear_queue_high(pointedobject)
                        pos = pointedobject.object:get_pos()
                        minetest.add_particlespawner(
                            {
                                amount = 16,
                                time = 0.25,
                                minpos = {
                                    x = pos.x - pointedobject.collisionbox[4],
                                    y = pos.y - pointedobject.collisionbox[4],
                                    z = pos.z - pointedobject.collisionbox[4]
                                },
                                maxpos = {
                                    x = pos.x + pointedobject.collisionbox[4],
                                    y = pos.y + pointedobject.collisionbox[4],
                                    z = pos.z + pointedobject.collisionbox[4]
                                },
                                minacc = {x = 0, y = 0.25, z = 0},
                                maxacc = {x = 0, y = -0.25, z = 0},
                                minexptime = 0.75,
                                maxexptime = 1,
                                minsize = 4,
                                maxsize = 4,
                                texture = "mob_core_green_particle.png",
                                glow = 16
                            })
                        return
                    end
                end
            else
                minetest.chat_send_player(name, "You must be pointing at a mob.")
                return
            end
        end
    end
})