--------------
-- Mob Meta --
--------------

-- Math --

local pi = math.pi
local pi2 = pi * 2
local abs = math.abs
local floor = math.floor
local random = math.random

local sin = math.sin
local cos = math.cos
local atan2 = math.atan2

local function diff(a, b) -- Get difference between 2 angles
	return math.atan2(math.sin(b - a), math.cos(b - a))
end

local function round(n, dec)
	local x = 10^(dec or 0)
	return math.floor(n * x + 0.5) / x
end

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

local function vec_center(v)
	return {x = floor(v.x + 0.5), y = floor(v.y + 0.5), z = floor(v.z + 0.5)}
end

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

local function fast_ray_sight(pos1, pos2)
	local ray = minetest.raycast(pos1, pos2, false, false)
	for pointed_thing in ray do
		if pointed_thing.type == "node" then
			return false
		end
	end
	return true
end

-- Local Utilities --

local default_node_def = {walkable = true} -- both ignore and unknown nodes are walkable

local function get_node_height(name)
	local def = minetest.registered_nodes[name]
	if not def then return 0.5 end
	if def.walkable then
		if def.drawtype == "nodebox" then
			if def.node_box
			and def.node_box.type == "fixed" then
				if type(def.node_box.fixed[1]) == "number" then
					return 0.5 + def.node_box.fixed[5]
				elseif type(def.node_box.fixed[1]) == "table" then
					return 0.5 + def.node_box.fixed[1][5]
				else
					return 1
				end
			else
				return 1
			end
		else
			return 1
		end
	else
		return 1
	end
end

local function get_node_def(name)
	local def = minetest.registered_nodes[name] or default_node_def
	if def.walkable
	and get_node_height(name) < 0.26 then
		def.walkable = false -- workaround for nodes like snow
	end
	return def
end

local function get_ground_level(pos2, max_diff)
	local node = minetest.get_node(pos2)
	local node_under = minetest.get_node({
		x = pos2.x,
		y = pos2.y - 1,
		z = pos2.z
	})
	local walkable = get_node_def(node_under.name) and not get_node_def(node.name)
	if walkable then
		return pos2
	end
	local diff = 0
	if not get_node_def(node_under.name) then
		for i = 1, max_diff do
			pos2.y = pos2.y - 1
			node = minetest.get_node(pos2)
			node_under = minetest.get_node({
				x = pos2.x,
				y = pos2.y - 1,
				z = pos2.z
			})
			walkable = get_node_def(node_under.name) and not get_node_def(node.name)
			if walkable then break end
		end
	else
		for i = 1, max_diff do
			pos2.y = pos2.y + 1
			node = minetest.get_node(pos2)
			node_under = minetest.get_node({
				x = pos2.x,
				y = pos2.y - 1,
				z = pos2.z
			})
			walkable = get_node_def(node_under.name) and not get_node_def(node.name)
			if walkable then break end
		end
	end
	return pos2
end

local function is_value_in_table(tbl, val)
    for _, v in pairs(tbl) do
        if v == val then
            return true
        end
    end
    return false
end

-------------------------
-- Physics/Vitals Tick --
-------------------------

local step_tick = 0.15

minetest.register_globalstep(function(dtime)
	if step_tick <= 0 then
		step_tick = 0.15
	end
	step_tick = step_tick - dtime
end)

-- A metatable is used to avoid issues
-- With mobs performing functions outside
-- their own scope

local mob = {
	-- Stats
	max_health = 20,
	armor_groups = {fleshy = 100},
	damage = 2,
	speed = 4,
	tracking_range = 16,
	despawn_after = nil,
	-- Physics
	max_fall = 3,
	stepheight = 1.1,
	hitbox = {
		width = 0.5,
		height = 1
	},
}

local mob_meta = {__index = mob}

local function index_box_border(self)
	local width = self.width
	local pos = self.object:get_pos()
	pos.y = pos.y + 0.5
	local pos1 = {
		x = pos.x - (width + 0.7),
		y = pos.y,
		z = pos.z - (width + 0.7),
	}
	local pos2 = {
		x = pos.x + (width + 0.7),
		y = pos.y,
		z = pos.z + (width + 0.7),
	}
	local border = {}
	for z = pos1.z, pos2.z do
		for x = pos1.x, pos2.x do
			local vec = {
				x = x,
				y = pos.y,
				z = z
			}
			if not self:pos_in_box(vec, width) then
				table.insert(border, vec_sub(vec, pos))
			end
		end
	end
	return border
end

function mob:indicate_damage()
	self._original_texture_mod = self._original_texture_mod or self.object:get_texture_mod()
	self.object:set_texture_mod(self._original_texture_mod .. "^[colorize:#FF000040")
	core.after(0.2, function()
		if creatura.is_alive(self) then
			self.object:set_texture_mod(self._original_texture_mod)
		end
	end)
end

-- Set Movement Data

function mob:move(pos, method, speed_factor, anim)
	self._movement_data.goal = pos
	self._movement_data.method = method
	self._movement_data.last_neighbor = nil
	self._movement_data.gravity = self._movement_data.gravity or -9.8
	self._movement_data.speed = (self.speed or 2) * (speed_factor or 1)
	if anim then
		self._movement_data.anim = anim
	end
end

-- Clear Movement Data

function mob:halt()
	self._movement_data = {
		goal = nil,
		method = nil,
		last_neighbor = nil,
		gravity = self._movement_data.gravity or -9.8,
		speed = 0
	}
	self._path_data = {}
end

-- Turn to specified yaw

function mob:turn_to(tyaw, rate)
	self._tyaw = tyaw
	local weight = rate or 10
	local yaw = self.object:get_yaw()

	yaw = yaw + pi
	tyaw = (tyaw + pi) % pi2

	local step = math.min(self.dtime * weight, abs(tyaw - yaw) % pi2)

	local dir = abs(tyaw - yaw) > pi and -1 or 1
	dir = tyaw > yaw and dir * 1 or dir * -1

	local nyaw = (yaw + step * dir) % pi2
	self.object:set_yaw(nyaw - pi)
	self.last_yaw = self.object:get_yaw()
end

-- Set Gravity (default of -9.8)

function mob:set_gravity(gravity)
	self._movement_data.gravity = gravity or -9.8
end

-- Sets Velocity to desired speed in mobs current look direction

function mob:set_forward_velocity(speed)
	local speed = speed or self._movement_data.speed
	local dir = minetest.yaw_to_dir(self.object:get_yaw())
	local vel = vec_multi(dir, speed)
	vel.y = self.object:get_velocity().y
	self.object:set_velocity(vel)
end

-- Sets Velocity on y axis

function mob:set_vertical_velocity(speed)
	local vel = self.object:get_velocity() or {x = 0, y = 0, z = 0}
	vel.y = speed
	self.object:set_velocity(vel)
end

-- Applies knockback in 'dir'

function mob:apply_knockback(dir, power)
	if not dir then return end
	power = power or 6
	if not self.touching_ground then
		power = power * 0.8
	end
	local knockback = vec_multi(dir, power)
	knockback.y = abs(power * 0.22)
	self.object:add_velocity(knockback)
end

-- Punch 'target'

function mob:punch_target(target) --
	target:punch(self.object, 1.0, {
		full_punch_interval = 1.0,
		damage_groups = {fleshy = self.damage or 5},
	})
end

-- Apply damage to mob

function mob:hurt(health)
	if self.protected then return end
	self.hp = self.hp - math.ceil(health)
end

-- Add HP to mob

function mob:heal(health)
	if self.protected then return end
	self.hp = self.hp + math.ceil(health)
	if self.hp > self.max_health then
		self.hp = self.max_health
	end
end

-- Return position at center of mobs hitbox

function mob:get_center_pos()
	return vec_raise(self.object:get_pos(), self.height * 0.5 or 0.5)
end

-- Return true if position is within box

function mob:pos_in_box(pos, size)
	if not pos then return false end
	local center = self:get_center_pos()
	local width = size or self.width
	local height = size or (self.height * 0.5)
	if not size
	and self.width < 0.5 then
		width = 0.5
	end
	local edge_a = {
		x = center.x - width,
		y = center.y - height,
		z = center.z - width
	}
	local edge_b = {
		x = center.x + width,
		y = center.y + height,
		z = center.z + width
	}
	local minp, maxp = vector.sort(edge_a, edge_b)
	if pos.x >= minp.x
	and pos.y >= minp.y
	and pos.z >= minp.z
	and pos.x <= maxp.x
	and pos.y <= maxp.y
	and pos.z <= maxp.z then
		return true
	end
	return false
end

-- Terrain Navigation --

function mob:get_wander_pos(min_range, max_range, dir)
	local pos = vec_center(self.object:get_pos())
	pos.y = floor(pos.y + 0.5)
	local node = minetest.get_node(pos)
	if get_node_def(node.name).walkable then -- Occurs if small mob is touching a fence
		local offset = vector.add(pos, vec_multi(vec_dir(pos, self.object:get_pos()), 1.5))
		pos.x = floor(offset.x + 0.5)
		pos.z = floor(offset.z + 0.5)
		pos = get_ground_level(pos, 1)
	end
	local width = self.width
	local outset = random(min_range, max_range)
	if width < 0.6 then width = 0.6 end
	local move_dir = vec_normal({
		x = random(-10, 10) * 0.1,
		y = 0,
		z = random(-10, 10) * 0.1
	})
	local pos2 = vec_add(pos, vec_multi(move_dir, width))
	if get_node_def(minetest.get_node(pos2).name).walkable
	and not dir then
		for i = 1, 3 do
			move_dir = {
				x = move_dir.z,
				y = 0,
				z = move_dir.x * -1
			}
			pos2 = vec_add(pos, vec_multi(move_dir, width))
			if not get_node_def(minetest.get_node(pos2).name).walkable then
				break
			end
		end
	elseif dir then
		move_dir = dir
	end
	for i = 1, outset do
		local a_pos = vec_add(pos2, vec_multi(move_dir, i))
		local a_node = minetest.get_node(a_pos)
		local b_pos = {x = a_pos.x, y = a_pos.y - 1, z = a_pos.z}
		local b_node = minetest.get_node(b_pos)
		if get_node_def(a_node.name).walkable
		or not get_node_def(b_node.name).walkable then
			a_pos = get_ground_level(a_pos, floor(self.stepheight or 1))
		end
		if not get_node_def(a_node.name).walkable then
			pos2 = a_pos
		else
			break
		end
	end
	return pos2
end

function mob:get_wander_pos_3d(min_range, max_range, dir, vert_bias)
	local pos = vec_center(self.object:get_pos())
	local node = minetest.get_node(pos)
	if get_node_def(node.name).walkable then -- Occurs if small mob is touching a fence
		local offset = vector.add(pos, vec_multi(vec_dir(pos, self.object:get_pos()), 1.5))
		pos.x = floor(offset.x + 0.5)
		pos.z = floor(offset.z + 0.5)
		pos = get_ground_level(pos, 1)
	end
	local width = self.width
	local outset = random(min_range, max_range)
	if width < 0.6 then width = 0.6 end
	local move_dir = vec_normal({
		x = random(-10, 10) * 0.1,
		y = vert_bias or random(-10, 10) * 0.1,
		z = random(-10, 10) * 0.1
	})
	local pos2 = vec_add(pos, vec_multi(move_dir, width))
	if get_node_def(minetest.get_node(pos2).name).walkable
	and not dir then
		for i = 1, 3 do
			move_dir = {
				x = move_dir.z,
				y = move_dir.y,
				z = move_dir.x * -1
			}
			pos2 = vec_add(pos, vec_multi(move_dir, width))
			if not get_node_def(minetest.get_node(pos2).name).walkable then
				break
			end
		end
	elseif dir then
		move_dir = dir
	end
	for i = 1, outset do
		local a_pos = vec_add(pos2, vec_multi(move_dir, i))
		local a_node = minetest.get_node(a_pos)
		if get_node_def(a_node.name).walkable then
			a_pos = get_ground_level(a_pos, floor(self.stepheight or 1))
		end
		if not get_node_def(a_node.name).walkable then
			pos2 = a_pos
		else
			break
		end
	end
	return pos2
end

function mob:is_pos_safe(pos)
	local mob_pos = self.object:get_pos()
	local node = minetest.get_node(pos)
	if not node then return false end
	if minetest.get_item_group(node.name, "igniter") > 0
	or get_node_def(node.name).drawtype == "liquid"
	or get_node_def(minetest.get_node(vec_raise(pos, -1)).name).drawtype == "liquid" then return false end
	local fall_safe = false
	if self.max_fall ~= 0 then
		for i = 1, self.max_fall or 3 do
			local fall_pos = {
				x = pos.x,
				y = floor(mob_pos.y + 0.5) - i,
				z = pos.z
			}
			if get_node_def(minetest.get_node(fall_pos).name).walkable then
				fall_safe = true
				break
			end
		end
	else
		fall_safe = true
	end
	return fall_safe
end

-- Set mobs animation (if specified animation isn't already playing)

function mob:animate(animation)
	if not animation
	or not self.animations[animation] then return end
	if not self._anim
	or self._anim ~= animation then
		local anim = self.animations[animation]
		self.object:set_animation(anim.range, anim.speed, anim.frame_blend, anim.loop)
		self._anim = animation
	end
end

-- Set texture to variable at 'id' index in 'tbl' or 'textures'

function mob:set_texture(id, tbl)
	local _table = self.textures
	if tbl then
		_table = tbl
	end
	if not _table
	or not _table[id] then
		return
	end
	self.object:set_properties({
		textures = {_table[id]}
	})
	return _table[id]
end

-- Set scale to base scale times 'x' and update bordering positions

function mob:set_scale(x)
	local def = minetest.registered_entities[self.name]
	local scale = def.visual_size
	local box = def.collisionbox
	local new_box = {}
	for k, v in ipairs(box) do
		new_box[k] = v * x
	end
	self.object:set_properties({
		visual_size = {
			x = scale.x * x,
			y = scale.y * x
		},
		collisionbox = new_box
	})
	self._border = index_box_border(self)
end

-- Fixes mob scale being changed when attached to a parent

function mob:fix_attached_scale(parent)
	local scale = self:get_visual_size()
	local parent_size = parent:get_properties().visual_size
	self.object:set_properties({
		visual_size = {
			x = scale.x / parent_size.x,
			y = scale.y / parent_size.y
		},
	})
end

-- Add sets 'id' to 'val' in permanent data

function mob:memorize(id, val)
	self.perm_data[id] = val
	return self.perm_data[id]
end

-- Remove 'id' from permanent data

function mob:forget(id)
	self.perm_data[id] = nil
end

-- Return value from 'id' in permanent data

function mob:recall(id)
	return self.perm_data[id]
end

-- Return true on interval specified by 'n'

function mob:timer(n)
	local t1 = floor(self.active_time)
	local t2 = floor(self.active_time + self.dtime)
	if t2 > t1 and t2%n == 0 then return true end
end

-- Play 'sound' from self.sounds

function mob:play_sound(sound)
	local spec = self.sounds and self.sounds[sound]
	local parameters = {object = self.object}

	if type(spec) == "table" then
		local name = spec.name
		if spec.variations then
			name = name .. "_" .. random(spec.variations)
		end
		local pitch = 1.0

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

		parameters.gain = spec.gain or 1
		parameters.max_hear_distance = spec.distance or 8
		parameters.fade = spec.fade or 1
		parameters.pitch = pitch
		return minetest.sound_play(name, parameters)
	end
	return minetest.sound_play(spec, parameters)
end

-- Return current collisionbox

function mob:get_hitbox()
	if not self.properties then return self.collisionbox end
	return self.properties.collisionbox
end

-- Return height of current collisionbox

function mob:get_height()
	local hitbox = self:get_hitbox()
	return hitbox[5] - hitbox[2]
end

-- Return current visual size

function mob:get_visual_size()
	if not self.properties then return end
	return self.properties.visual_size
end

local function is_group_in_table(tbl, name)
	for _, v in pairs(tbl) do
		if minetest.get_item_group(name, v:split(":")[2]) > 0 then
			return true
		end
	end
	return false
end

function mob:follow_wielded_item(player)
	if not player
	or not self.follow then return end
	local item = player:get_wielded_item()
	local name = item:get_name()
	if type(self.follow) == "string"
	and (name == self.follow
	or minetest.get_item_group(name, self.follow:split(":")[2]) > 0) then
		return item, name
	end
	if type(self.follow) == "table"
	and (is_value_in_table(self.follow, name)
	or is_group_in_table(self.follow, name)) then
		return item, name
	end
end

function mob:get_target(target)
	local alive = creatura.is_alive(target)
	if not alive then
		return false, false, nil
	end
	if type(target) == "table" then
		target = target.object
	end
	local pos = self:get_center_pos()
	local tpos = target:get_pos()
	tpos.y = floor(tpos.y + 0.5)
	local line_of_sight = fast_ray_sight(pos, tpos)
	return true, line_of_sight, tpos
end

-- Actions

function mob:set_action(func)
	self._action = func
end

function mob:get_action()
	if type(self._action) ~= "table" then
		return self._action
	end
	return nil
end

function mob:clear_action()
	self._action = {}
end

function mob:set_utility(func)
	self._utility_data.func = func
end

function mob:get_utility()
	if not self._utility_data then return end
	return self._utility_data.utility
end

function mob:initiate_utility(utility, ...)
	local func = creatura.registered_utilities[utility]
	if not func or not self._utility_data then return end
	self._utility_data.utility = utility
	self:clear_action()
	func(...)
end

function mob:set_utility_score(n)
	self._utility_data.score = n or 0
end

function mob:try_initiate_utility(utility, score, ...)
	if self._utility_data
	and score >= self._utility_data.score then
		self:initiate_utility(utility, ...)
		self:set_utility_score(score)
	end
end

function mob:clear_utility()
	self._utility_data = {
		utility = nil,
		func = nil,
		score = 0
	}
end

-- Functions

function mob:activate(staticdata, dtime)
	self.properties = self.object:get_properties()
	self.width = self:get_hitbox()[4] or 0.5
	self.height = self:get_height() or 1
	self._tyaw = self.object:get_yaw()
	self.last_yaw = self.object:get_yaw()
	self.active_time = 0
	self.in_liquid = false
	self.is_falling = false
	self.touching_ground = false

	-- Backend Data (Should not be modified unless modder knows what they're doing)
	self._movement_data = {
		goal = nil,
		method = nil,
		last_neighbor = nil,
		gravity = -9.8,
		speed = 0
	}
	self._path_data = {}
	self._path = {}
	self._task = {}
	self._action = {}

	local pos = self.object:get_pos()
	local node = minetest.get_node(pos)

	if node
	and minetest.get_item_group(node.name, "liquid") > 0 then
		self.in_liquid = node.name
	end

	-- Staticdata
	if staticdata then
		local data = minetest.deserialize(staticdata)
		if data then
			for k, v in pairs(data) do
				self[k] = v
			end
		end
	end

	-- Initialize Stats and Visuals
	if not self.textures then
		local textures = self.properties.textures
		if textures then self.textures = textures end
	end

	if not self.perm_data then
		if self.memory then
			self.perm_data = self.memory
		else
			self.perm_data = {}
		end
		if #self.textures > 1 then self.texture_no = random(#self.textures) end
	end

	if self:recall("despawn_after") ~= nil then
		self.despawn_after = self:recall("despawn_after")
	end
	self._despawn = self:recall("_despawn") or false

	if self._despawn
	and self.despawn_after then
		self.object:remove()
		return
	end

	self._breath =  self:recall("_breath") or (self.max_breath or 30)
	self._border = index_box_border(self)

	if self.textures
	and self.texture_no then
		self:set_texture(self.texture_no, self.textures)
	end

	self.max_health = self.max_health or 10
	self.hp = self.hp or self.max_health

	if type(self.armor_groups) ~= "table" then
		self.armor_groups = {}
	end
	self.armor_groups.immortal = 1
	self.object:set_armor_groups(self.armor_groups)

	if self.timer
	and type(self.timer) == "number" then -- fix crash for converted mobs_redo mobs
		self.timer = function(self, n)
			local t1 = floor(self.active_time)
			local t2 = floor(self.active_time + self.dtime)
			if t2 > t1 and t2%n == 0 then return true end
		end
	end

	if self.activate_func then
		self:activate_func(self, staticdata, dtime)
	end
end

function mob:staticdata()
	local data = {}
	data.perm_data = self.perm_data
	data.hp = self.hp or self.max_health
	data.texture_no = self.texture_no or random(#self.textures)
	return minetest.serialize(data)
end

function mob:on_step(dtime, moveresult)
	--local us_time = minetest.get_us_time()
	if not self.hp then return end
	self.dtime = dtime or 0.09
	self.moveresult = moveresult or {}
	self.touching_ground = false
	if moveresult then
		self.touching_ground = moveresult.touching_ground
	end
	if step_tick <= 0 then
		-- Physics and Vitals
		if self._physics then
			self:_physics(moveresult)
		end
		if self._vitals then
			self:_vitals()
		end
		-- Cached Geometry
		self.properties = self.object:get_properties()
		self.width = self:get_hitbox()[4] or 0.5
		self.height = self:get_height() or 1
	end
	self:_light_physics()
	-- Movement Control
	if self._move then
		self:_move()
	end
	if self.utility_stack
	and self._execute_utilities then
		self:_execute_utilities()
		self:_execute_actions()
	end
	-- Die
	if self.hp <= 0
	and self.death_func then
		self:death_func()
		self:halt()
		return
	end
	if self.step_func
	and self.perm_data then
		self:step_func(dtime, moveresult)
	end
	self.active_time = self.active_time + dtime
	if self.despawn_after
	and self.active_time >= self.despawn_after then
		self._despawn = self:memorize("_despawn", true)
	end
end

function mob:on_deactivate()
	self._task = {}
	self._action = {}
	if self.deactivate_func then
		self:deactivate_func(self)
	end
end

----------------
-- Object API --
----------------

local fancy_step = false

local step_type = minetest.settings:get("creatura_step_type")

if step_type == "fancy" then
	fancy_step = true
end

-- Physics

local moveable = creatura.is_pos_moveable

local function do_step(self)
	if not fancy_step then return end
	local pos = self.object:get_pos()
	local vel = self.object:get_velocity()
	if not self._step then
		if self.touching_ground
		and abs(vel.x + vel.z) > 0 then
			local border = self._border
			local yaw_offset = vec_add(pos, vec_multi(minetest.yaw_to_dir(self.object:get_yaw()), self.width + 0.7))
			table.sort(border, function(a, b) return vec_dist(vec_add(pos, a), yaw_offset) < vec_dist(vec_add(pos, b), yaw_offset) end)
			local step_pos = vec_center(vec_add(pos, border[1]))
			local halfway = vec_add(pos, vec_multi(vec_dir(pos, step_pos), 0.5))
			halfway.y = step_pos.y
			if creatura.get_node_def(step_pos).walkable
			and abs(diff(self.object:get_yaw(), minetest.dir_to_yaw(vec_dir(pos, step_pos)))) < 1.5
			and moveable(halfway, self.width, self.height) then
				self._step = vec_center(step_pos)
			end
		end
	else
		local vel = self.object:get_velocity()
		self.object:set_velocity(vector.new(vel.x, 7, vel.z))
		if self._step.y < pos.y - 0.5 then
			self.object:set_velocity(vector.new(vel.x, 0.5, vel.z))
			self._step = nil
			local step_pos = self.object:get_pos()
			local dir = minetest.yaw_to_dir(self.object:get_yaw())
			step_pos = vec_add(step_pos, vec_multi(dir, 0.1))
			self.object:set_pos(step_pos)
		end
	end
end

local function collision_detection(self)
	if not creatura.is_alive(self) then return end
	local pos = self.object:get_pos()
	local width = self.width + 0.25
	local objects = minetest.get_objects_in_area(vec_sub(pos, width), vec_add(pos, width))
	if #objects < 2 then return end
	local col_no = 0
	for i = 2, #objects do
		local object = objects[i]
		if creatura.is_alive(object)
		and not self.object:get_attach()
		and not object:get_attach() then
			if i > 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 = vec_multi(dir, 1.1)
			local vel1 = vec_multi(velocity, -2) -- multiplying by -2 accounts for friction
			local vel2 = velocity
			self.object:add_velocity(vel1)
			object:add_velocity(vel2)
		end
	end
end

local function water_physics(self)
	-- Props
	local gravity = self._movement_data.gravity
	local height = self.height
	-- Vectors
	local floor_pos = self.object:get_pos()
	floor_pos.y = floor_pos.y + 0.01
	local surface_pos = floor_pos
	local floor_node = minetest.get_node(floor_pos)
	local surface_node = minetest.get_node(surface_pos)
	if minetest.get_item_group(floor_node.name, "liquid") < 1 then
		self.object:set_acceleration({
			x = 0,
			y = gravity,
			z = 0
		})
		if self.in_liquid then
			self.in_liquid = false
		end
		return
	end
	self.in_liquid = floor_node.name
	-- Get submergence (Not the most accurate, but reduces lag)
	for i = 1, math.ceil(height * 3) do
		local step_pos = {
			x = floor_pos.x,
			y = floor_pos.y + 0.5 * i,
			z = floor_pos.z
		}
		if minetest.get_item_group(minetest.get_node(step_pos).name, "liquid") > 0 then
			surface_pos = step_pos
		else
			break
		end
	end
	-- Apply Physics
	local submergence = surface_pos.y - floor_pos.y
	local vel = self.object:get_velocity()
	local bouyancy = self.bouyancy_multiplier or 1
	self.object:set_acceleration({
		x = 0,
		y = (submergence - vel.y * abs(vel.y) * 0.4) * bouyancy,
		z = 0
	})
	local hydrodynamics = self.hydrodynamics_multiplier or 0.7
	local vel_y = vel.y
	if self.bouyancy_multiplier == 0 then -- if bouyancy is disabled drag will be applied to keep awuatic mobs from drifting
		vel_y = vel.y * hydrodynamics
	end
	self.object:set_velocity({
		x = vel.x * hydrodynamics,
		y = vel_y,
		z = vel.z * hydrodynamics
	})
end

function mob:_physics(moveresult)
	if not self.object then return end
	water_physics(self)
	-- Step up nodes
	do_step(self, moveresult)
	-- Object collision
	collision_detection(self)
	if not self.in_liquid
	and not self.touching_ground then
		self.is_falling = true
	else
		self.is_falling = false
	end
	if not self.in_liquid
	and self._movement_data.gravity ~= 0 then
		local vel = self.object:get_velocity()
		if self.touching_ground then
			local nvel = vector.multiply(vel, 0.2)
			if nvel.x < 0.2
			and nvel.z < 0.2 then
				nvel.x = 0
				nvel.z = 0
			end
			nvel.y = vel.y
			self.object:set_velocity(nvel)
		else
			local nvel = vector.multiply(vel, 0.1)
			if nvel.x < 0.2
			and nvel.z < 0.2 then
				nvel.x = 0
				nvel.z = 0
			end
			nvel.y = vel.y
			self.object:set_velocity(nvel)
		end
	end
end

function mob:_light_physics() -- physics that are lightweight enough to be called each step
end

-- Movement Control

function mob:_move()
	if not self.object then return end
	local data = self._movement_data
	local speed = data.speed
	if data.goal then
		local pos = data.goal
		local method = data.method
		local anim = data.anim
		if creatura.registered_movement_methods[method] then
			local func = creatura.registered_movement_methods[method]
			func(self, pos, speed, anim)
		end
	end
end

-- Execute Actions

function mob:_execute_actions()
	if not self.object then return end
	local task = self._task
	if #self._task > 0 then
		local func = self._task[#self._task].func
		if func(self) then
			self._task[#self._task] = nil
			self:clear_action()
			return
		end
	end
	local action = self._action
	if type(action) ~= "table" then
		local func = action
		if func(self) then
			self:clear_action()
		end
	end
end

local function tbl_equals(tbl1, tbl2)
	local match = true
	for k, v in pairs(tbl1) do
		if not tbl2[k]
		and tbl2[k] ~= v then
			match = false
			break
		end
	end
	return match
end

function mob:_execute_utilities()
	local is_alive = self.hp > 0
	if not self._utility_data then
		self._utility_data = {
			utility = nil,
			func = nil,
			score = 0
		}
	end
	local loop_data = {
		utility = nil,
		func = nil,
		score = 0
	}
	if (self:timer(self.task_timer or 1)
	or not self._utility_data.func)
	and is_alive then
		for i = 1, #self.utility_stack do
			local utility = self.utility_stack[i].utility
			local get_score = self.utility_stack[i].get_score
			local score, args = get_score(self)
			if self._utility_data.utility
			and utility == self._utility_data.utility
			and self._utility_data.score > 0
			and score <= 0 then
				self._utility_data = {
					utility = nil,
					func = nil,
					score = 0
				}
			end
			if score > 0
			and score >= self._utility_data.score
			and score >= loop_data.score then
				loop_data = {
					utility = utility,
					score = score,
					args = args
				}
			end
		end
	end
	if loop_data.utility
	and loop_data.args then
		if not self._utility_data
		or not self._utility_data.args then
			self._utility_data = loop_data
		else
			local no_data = not self._utility_data.utility and not self._utility_data.args
			local new_util = self._utility_data.utility ~= loop_data.utility or not tbl_equals(self._utility_data.args, loop_data.args)
			if no_data
			or new_util then -- if utilities are different or utilities are the same and args are different set new data
				self._utility_data = loop_data
			end
		end
	end
	if self._utility_data.utility then
		if not self._utility_data.func then
			self:initiate_utility(self._utility_data.utility, unpack(self._utility_data.args))
		end
		local func = self._utility_data.func
		if not func then return end
		if func(self) then
			self._utility_data = {
				utility = nil,
				func = nil,
				score = 0
			}
			self:clear_action()
		end
	end
end

-- Vitals

function mob:_vitals()
	local stand_pos = self.object:get_pos()
	local fall_start = self._fall_start
	if self.is_falling
	and not fall_start
	and self.max_fall > 0 then
		self._fall_start = stand_pos.y
	elseif fall_start
	and self.max_fall > 0 then
		if self.touching_ground
		and not self.in_liquid then
			local damage = fall_start - stand_pos.y
			if damage < (self.max_fall or 3) then
				self._fall_start = nil
				return
			end
			local resist = self.fall_resistance or 0
			self:hurt(damage - (damage * (resist * 0.1)))
			self:indicate_damage()
			if random(4) < 2 then
				self:play_sound("hurt")
			end
			self._fall_start = nil
		elseif self.in_liquid then
			self._fall_start = nil
		end
	end
	if self:timer(1) then
		local head_pos = vec_raise(stand_pos, self.height)
		local head_def = get_node_def(minetest.get_node(head_pos).name)
		if head_def.drawtype == "liquid"
		and minetest.get_item_group(minetest.get_node(head_pos).name, "water") > 0 then
			if self._breath <= 0 then
				self:hurt(1)
				self:indicate_damage()
				if random(4) < 2 then
					self:play_sound("hurt")
				end
			else
				self._breath = self._breath - 1
				self:memorize("_breath", self._breath)
			end
		end
		local stand_def = get_node_def(minetest.get_node(stand_pos).name)
		if minetest.get_item_group(minetest.get_node(stand_pos).name, "fire") > 0
		and stand_def.damage_per_second then
			local damage = stand_def.damage_per_second
			local resist = self.fire_resistance or 0
			self:hurt(damage - (damage * (resist * 0.1)))
			self:indicate_damage()
			if random(4) < 2 then
				self:play_sound("hurt")
			end
		end
	end
	if self:timer(5) then
		local objects = minetest.get_objects_inside_radius(stand_pos, 0.2)
		if #objects > 10 then
			self:indicate_damage()
			self.hp = self:memorize("hp", -1)
			self:death_func()
		end
	end
end

function creatura.register_mob(name, def)
	local box_width = def.hitbox and def.hitbox.width or 0.5
	local box_height = def.hitbox and def.hitbox.height or 1
	local hitbox = {-box_width, 0, -box_width, box_width, box_height, box_width}

	def.physical = def.physical or true
	def.collide_with_objects = def.collide_with_objects or false
	def.visual = "mesh"
	def.makes_footstep_sound = def.makes_footstep_sound or false
	if def.static_save ~= false then
		def.static_save = true
	end
	def.collisionbox = hitbox
	def._creatura_mob = true

	def.sounds = def.sounds or {}

	if not def.sounds.hit then
		def.sounds.hit = {
			name = "creatura_hit",
			gain = 0.5,
			distance = 16,
			variations = 3
		}
	end

	def.on_activate = function(self, staticdata, dtime)
		return self:activate(staticdata, dtime)
	end

	def.get_staticdata = function(self)
		return self:staticdata(self)
	end

	minetest.register_entity(name, setmetatable(def, mob_meta))
end