local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs
local mob_class = mcl_mobs.mob_class

local damage_enabled = minetest.settings:get_bool("enable_damage")
local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false

-- pathfinding settings
local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching
local stuck_path_timeout = 10 -- how long will mob follow path before giving up

local enable_pathfinding = true

local TIME_TO_FORGET_TARGET = 15
local PI = math.pi
local HALFPI = PI * 0.5
local random = math.random
local min = math.min
local floor = math.floor
local ceil = math.ceil
local abs = math.abs
local cos = math.cos
local sin = math.sin
local atan2 = math.atan2
local vector_offset = vector.offset
local vector_new = vector.new
local vector_copy = vector.copy
local vector_distance = vector.distance

-- check if daytime and also if mob is docile during daylight hours
function mob_class:day_docile()
	return self.docile_by_day == true and self.time_of_day > 0.2 and self.time_of_day < 0.8
end

-- get this mob to attack the object
function mob_class:do_attack(object)
	if self.state == "attack" or self.state == "die" then return end
	if object:is_player() and not minetest.settings:get_bool("enable_damage") then return end

	self.attack = object
	self.state = "attack"

	-- TODO: Implement war_cry sound without being annoying
	--if random(0, 100) < 90 then
		--self:mob_sound("war_cry", true)
	--end
end

-- blast damage to entities nearby
local function entity_physics(pos, radius)
	radius = radius * 2

	local objs = minetest.get_objects_inside_radius(pos, radius)
	local obj_pos, dist
	for n = 1, #objs do
		obj_pos = objs[n]:get_pos()

		dist = vector_distance(pos, obj_pos)
		if dist < 1 then dist = 1 end

		local damage = floor((4 / dist) * radius)
		local ent = objs[n]:get_luaentity()

		-- punches work on entities AND players
		objs[n]:punch(objs[n], 1.0, {
			full_punch_interval = 1.0,
			damage_groups = {fleshy = damage},
		}, pos)
	end
end

function mob_class:entity_physics(pos,radius) return entity_physics(pos,radius) end

local los_switcher = false
local height_switcher = false

-- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3
function mob_class:smart_mobs(s, p, dist, dtime)
	local s1 = self.path.lastpos
	local target_pos = self.attack:get_pos()

	-- is it becoming stuck?
	if abs(s1.x - s.x) + abs(s1.z - s.z) < .5 then
		self.path.stuck_timer = self.path.stuck_timer + dtime
	else
		self.path.stuck_timer = 0
	end

	self.path.lastpos = vector_copy(s)

	local use_pathfind = false
	local has_lineofsight = minetest.line_of_sight(vector_offset(s, 0, .5, 0), vector_offset(target_pos, 0, 1.5, 0), .2)

	-- im stuck, search for path
	if not has_lineofsight then
		if los_switcher == true then
			use_pathfind = true
			los_switcher = false
		end -- cannot see target!
	else
		if los_switcher == false then
			los_switcher = true
			use_pathfind = false
			minetest.after(1, function(self)
				if not self.object:get_luaentity() then return end
				if has_lineofsight then self.path.following = false end
			end, self)
		end -- can see target!
	end

	if (self.path.stuck_timer > stuck_timeout and not self.path.following) then
		use_pathfind = true
		self.path.stuck_timer = 0
		minetest.after(1, function(self)
			if not self.object:get_luaentity() then return end
			if has_lineofsight then self.path.following = false end
		end, self)
	end

	if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then
		use_pathfind = true
		self.path.stuck_timer = 0
		minetest.after(1, function(self)
			if not self.object:get_luaentity() then return end
			if has_lineofsight then self.path.following = false end
		end, self)
	end

	if abs(s.y - target_pos.y) > self.initial_properties.stepheight then
		if height_switcher then
			use_pathfind = true
			height_switcher = false
		end
	else
		if not height_switcher then
			use_pathfind = false
			height_switcher = true
		end
	end

	if use_pathfind then
		-- lets try find a path, first take care of positions
		-- since pathfinder is very sensitive
		local cb = self.initial_properties.collisionbox
		local sheight = cb[5] - cb[2]

		-- round position to center of node to avoid stuck in walls
		-- also adjust height for player models!
		s.x, s.z = floor(s.x + 0.5), floor(s.z + 0.5)

		local ssight, sground = minetest.line_of_sight(s, vector_offset(s, 0, -4, 0), 1)

		-- determine node above ground
		if not ssight then s.y = sground.y + 1 end

		local p1 = self.attack:get_pos()
		p1 = vector_new(floor(p1.x + 0.5), floor(p1.y + 0.5), floor(p1.z + 0.5))

		local dropheight = 12
		if self.fear_height ~= 0 then dropheight = self.fear_height end
		local jumpheight = 0
		if self.jump and self.jump_height >= 4 then
			jumpheight = min(ceil(self.jump_height * 0.25), 4)
		elseif self.initial_properties.stepheight > 0.5 then
			jumpheight = 1
		end
		self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch")

		self.state = ""
		self:do_attack(self.attack)

		-- no path found, try something else
		if not self.path.way then
			self.path.following = false

			 -- lets make way by digging/building if not accessible
			if self.pathfinding == 2 and mobs_griefing then
				-- is player higher than mob?
				if s.y < p1.y then
					-- build upwards
					if not minetest.is_protected(s, "") then
						local ndef1 = minetest.registered_nodes[self.standing_in]
						if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then
								minetest.set_node(s, {name = mcl_mobs.fallback_node})
						end
					end

					local sheight = ceil(self.initial_properties.collisionbox[5]) + 1

					-- assume mob is 2 blocks high so it digs above its head
					s.y = s.y + sheight

					-- remove one block above to make room to jump
					if not minetest.is_protected(s, "") then
						local node1 = node_ok(s, "air").name
						local ndef1 = minetest.registered_nodes[node1]

						if node1 ~= "air"
						and node1 ~= "ignore"
						and ndef1
						and not ndef1.groups.level
						and not ndef1.groups.unbreakable
						and not ndef1.groups.liquid then
							minetest.set_node(s, {name = "air"})
							minetest.add_item(s, ItemStack(node1))
						end
					end

					s.y = s.y - sheight
					self.object:set_pos(vector_offset(s, 0, 2, 0))
				else -- dig 2 blocks to make door toward player direction
					local yaw1 = self.object:get_yaw() + HALFPI
					local p1 = vector_offset(s, cos(yaw1), 0, sin(yaw1))

					if not minetest.is_protected(p1, "") then
						local node1 = node_ok(p1, "air").name
						local ndef1 = minetest.registered_nodes[node1]
						if node1 ~= "air" and node1 ~= "ignore"
							and ndef1
							and not ndef1.groups.level
							and not ndef1.groups.unbreakable
							and not ndef1.groups.liquid then

							minetest.add_item(p1, ItemStack(node1))
							minetest.set_node(p1, {name = "air"})
						end

						p1.y = p1.y + 1
						node1 = node_ok(p1, "air").name
						ndef1 = minetest.registered_nodes[node1]

						if node1 ~= "air" and node1 ~= "ignore"
						and ndef1
						and not ndef1.groups.level
						and not ndef1.groups.unbreakable
						and not ndef1.groups.liquid then

							minetest.add_item(p1, ItemStack(node1))
							minetest.set_node(p1, {name = "air"})
						end

					end
				end
			end

			-- will try again in 2 seconds
			self.path.stuck_timer = stuck_timeout - 2
		elseif s.y < p1.y and (not self.fly) then
			self:do_jump() --add jump to pathfinding
			self.path.following = true
			-- Yay, I found path!
			-- TODO: Implement war_cry sound without being annoying
			--self:mob_sound("war_cry", true)
		else
			self:set_velocity(self.walk_velocity)

			-- follow path now that it has it
			self.path.following = true
		end
	end
end


-- specific attacks
local specific_attack = function(list, what)
	-- no list so attack default (player, animals etc.)
	if list == nil then return true end

	-- found entity on list to attack?
	for no = 1, #list do
		if list[no] == what then return true end
	end
	return false
end

-- find someone to attack
function mob_class:monster_attack()
	if not damage_enabled or self.passive ~= false or self.state == "attack" or self:day_docile() then return end

	local s = self.object:get_pos()
	local p, sp, dist
	local player, obj, min_player
	local type, name = "", ""
	local min_dist = self.view_range + 1

	local blacklist_attack = {}

	local objs = minetest.get_objects_inside_radius(s, self.view_range)

	for n = 1, #objs do
		if not objs[n]:is_player() then
			obj = objs[n]:get_luaentity()

			if obj then
				player = obj.object
				name = obj.name or ""
			end
			if obj and obj.type == self.type and obj.passive == false and obj.state == "attack" and obj.attack then
				table.insert(blacklist_attack, obj.attack)
			end
		end
	end

	for n = 1, #objs do
		if objs[n]:is_player() then
			if mcl_mobs.invis[ objs[n]:get_player_name() ] or (not self:object_in_range(objs[n])) then
				type = ""
			elseif (self.type == "monster" or self._aggro) then
				-- self.aggro made player be attacked by npc again if out of range then back in again
				-- Does it serve a purpose other than that?
				player = objs[n]
				type = "player"
				name = "player"
			end
		else
			obj = objs[n]:get_luaentity()
			if obj then
				player = obj.object
				type = obj.type
				name = obj.name or ""
			end
		end

		-- find specific mob to attack, failing that attack player/npc/animal
		if specific_attack(self.specific_attack, name)
				and (type == "player" or ( type == "npc" and self.attack_npcs )
				or (type == "animal" and self.attack_animals == true)
				or (self.extra_hostile and not self.attack_exception(player))) then
			p = player:get_pos()
			sp = s

			dist = vector_distance(p, s)

			-- aim higher to make looking up hills more realistic
			p.y = p.y + 1
			sp.y = sp.y + 1

			local attacked_p = false
			for c=1, #blacklist_attack do
				if blacklist_attack[c] == player then
					attacked_p = true
				end
			end

			-- choose closest player to attack
			local line_of_sight = self:line_of_sight( sp, p, 2) == true
			if dist < min_dist and not attacked_p and line_of_sight then
				min_dist = dist
				min_player = player
			end
		end
	end
	if not min_player and #blacklist_attack > 0 then
		min_player=blacklist_attack[random(#blacklist_attack)]
	end
	-- attack player
	if min_player then
		self:do_attack(min_player)
	end
end


-- npc, find closest monster to attack
function mob_class:npc_attack()
	if self.type ~= "npc"
	or not self.attacks_monsters
	or self.state == "attack" then
		return
	end

	local p, sp, obj, min_player
	local s = self.object:get_pos()
	local min_dist = self.view_range + 1
	local objs = minetest.get_objects_inside_radius(s, self.view_range)

	for n = 1, #objs do
		obj = objs[n]:get_luaentity()

		if obj and obj.type == "monster" then
			p = obj.object:get_pos()
			sp = s

			local dist = vector_distance(p, s)

			-- aim higher to make looking up hills more realistic
			p.y = p.y + 1
			sp.y = sp.y + 1

			if dist < min_dist and self:line_of_sight( sp, p, 2) == true then
				min_dist = dist
				min_player = obj.object
			end
		end
	end

	if min_player then
		self:do_attack(min_player)
	end
end



-- dogshoot attack switch and counter function
function mob_class:dogswitch(dtime)
	-- switch mode not activated
	if not self.dogshoot_switch or not dtime then return 0 end

	self.dogshoot_count = self.dogshoot_count + dtime

	if (self.dogshoot_switch == 1 and self.dogshoot_count > self.dogshoot_count_max)
	or (self.dogshoot_switch == 2 and self.dogshoot_count > self.dogshoot_count2_max) then

		self.dogshoot_count = 0

		if self.dogshoot_switch == 1 then
			self.dogshoot_switch = 2
		else
			self.dogshoot_switch = 1
		end
	end

	return self.dogshoot_switch
end

-- no damage to nodes explosion
function mob_class:safe_boom(pos, strength)
	minetest.sound_play(self.sounds and self.sounds.explode or "tnt_explode", {
		pos = pos,
		gain = 1.0,
		max_hear_distance = self.sounds and self.sounds.distance or 32
	}, true)
	local radius = strength
	entity_physics(pos, radius)
	mcl_mobs.effect(pos, 32, "mcl_particles_smoke.png", radius * 3, radius * 5, radius, 1, 0)
end


-- make explosion with protection and tnt mod check
function mob_class:boom(pos, strength, fire)
	if mobs_griefing and not minetest.is_protected(pos, "") then
		mcl_explosions.explode(pos, strength, { drop_chance = 1.0, fire = fire }, self.object)
	else
		mcl_mobs.mob_class.safe_boom(self, pos, strength) --need to call it this way bc self is the "arrow" object here
	end

	-- delete the object after it punched the player to avoid nil entities in e.g. mcl_shields!!
	mcl_util.remove_entity(self)
end

-- deal damage and effects when mob punched
function mob_class:on_punch(hitter, tflp, tool_capabilities, dir)
	local is_player = hitter:is_player()
	local mob_pos = self.object:get_pos()
	local player_pos = hitter:get_pos()
	local weapon = hitter:get_wielded_item()

	if is_player then
		-- is mob out of reach?
		if (vector.distance(mob_pos, player_pos) - self._avg_radius) > (weapon:get_definition().range or 3) then
			return
		end
		-- is mob protected?
		if self.protected and minetest.is_protected(mob_pos, hitter:get_player_name()) then return end

		mcl_potions.update_haste_and_fatigue(hitter)
	end

	local time_now = minetest.get_us_time()
	local time_diff = time_now - self.invul_timestamp

	-- check for invulnerability time in microseconds (0.5 second)
	if time_diff <= 500000 and time_diff >= 0 then return end

	-- custom punch function
	if self.do_punch then
		-- when false skip going any further
		if self.do_punch(self, hitter, tflp, tool_capabilities, dir) == false then
			return
		end
	end

	-- error checking when mod profiling is enabled
	if not tool_capabilities then
		minetest.log("warning", "[mobs] Mod profiling enabled, damage not enabled")
		return
	end

	local time_now = minetest.get_us_time()

	if is_player then
		if minetest.is_creative_enabled(hitter:get_player_name()) then self.health = 0 end
		-- set/update 'drop xp' timestamp if hitted by player
		self.xp_timestamp = time_now
	end

	-- punch interval
	local punch_interval = 1.4

	-- exhaust attacker
	if is_player then
		mcl_hunger.exhaust(hitter:get_player_name(), mcl_hunger.EXHAUST_ATTACK)
	end

	-- calculate mob damage
	local damage = 0
	local armor = self.object:get_armor_groups() or {}
	local tmp

	-- quick error check incase it ends up 0 (serialize.h check test)
	if tflp == 0 then
		tflp = 0.2
	end


	for group,_ in pairs((tool_capabilities.damage_groups or {}) ) do
		tmp = tflp / (tool_capabilities.full_punch_interval or 1.4)
		tmp = tmp < 0 and 0 or (tmp > 1 and 1 or tmp)
		damage = damage + (tool_capabilities.damage_groups[group] or 0) * tmp * ((armor[group] or 0) / 100.0)
	end

	-- strength and weakness effects
	local strength = mcl_potions.get_effect(hitter, "strength")
	local weakness = mcl_potions.get_effect(hitter, "weakness")
	local str_fac = strength and strength.factor or 1
	local weak_fac = weakness and weakness.factor or 1
	damage = damage * str_fac * weak_fac

	if weapon then
		local fire_aspect_level = mcl_enchanting.get_enchantment(weapon, "fire_aspect")
		if fire_aspect_level > 0 then
			mcl_burning.set_on_fire(self.object, fire_aspect_level * 4)
		end
	end

	-- check for tool immunity or special damage
	for n = 1, #self.immune_to do
		if self.immune_to[n][1] == weapon:get_name() then
			damage = self.immune_to[n][2] or 0
			break
		end
	end

	-- healing
	if damage <= -1 then
		self.health = self.health - floor(damage)
		return
	end

	if tool_capabilities then
		punch_interval = tool_capabilities.full_punch_interval or 1.4
	end

	if not tflp then
		tflp = punch_interval
	end

	-- add weapon wear manually
	-- Required because we have custom health handling ("health" property)
	if minetest.is_creative_enabled("") ~= true
	and tool_capabilities then
		if tool_capabilities.punch_attack_uses then
			-- Without this delay, the wear does not work. Quite hacky ...
			minetest.after(0, function(name)
				local player = minetest.get_player_by_name(name)
				if not player then return end
				local weapon = hitter:get_wielded_item(player)
				local def = weapon:get_definition()
				if def.tool_capabilities and def.tool_capabilities.punch_attack_uses then
					local wear = floor(65535/tool_capabilities.punch_attack_uses)
					weapon:add_wear(wear)
					tt.reload_itemstack_description(weapon) -- update tooltip
					hitter:set_wielded_item(weapon)
				end
			end, hitter:get_player_name())
		end
	end

	local die = false

	if damage >= 0 then
		local hv = hitter:get_velocity() or vector.zero()
		if hv.y < 0 then
			self:crit_effect()
			minetest.sound_play("mcl_criticals_hit", {object = self.object})
			local crit_mod
			local CRIT_MIN = 1.5
			local CRIT_DIFF = 1
			if is_player then
				local luck = mcl_luck.get_luck(hitter:get_player_name())
				if luck ~= 0 then
					local a, d
					if luck > 0 then
						d = -0.5
						a = d - math.abs(luck)
					elseif luck < 0 then
						a = -0.5
						d = a - math.abs(luck)
					else
						minetest.log("warning", "[mcl_mobs] luck is not a number") -- this technically can't happen, but want to catch such cases
					end
					if a then
						local x = math.random()
						crit_mod = CRIT_DIFF * (a * x) / (d - luck * x) + CRIT_MIN
					end
				end
			end
			if not crit_mod then
				crit_mod = math.random(CRIT_MIN, CRIT_MIN + CRIT_DIFF)
			end
			damage = damage * crit_mod
		end
		-- only play hit sound and show blood effects if damage is 1 or over; lower to 0.1 to ensure armor works appropriately.
		if damage >= 0.1 then
			-- weapon sounds
			if weapon:get_definition().sounds ~= nil then
				local s = random(0, #weapon:get_definition().sounds)

				minetest.sound_play(weapon:get_definition().sounds[s], {
					object = self.object, --hitter,
					max_hear_distance = 8
				}, true)
			else
				minetest.sound_play("default_punch", {
					object = self.object,
					max_hear_distance = 5
				}, true)
			end

			self:damage_effect(damage)

			-- do damage
			self.health = self.health - damage

			-- give invulnerability
			self.invul_timestamp = time_now

			-- skip future functions if dead, except alerting others
			if self:check_for_death( "hit", {type = "punch", puncher = hitter}) then
				die = true
			end
		end
		-- knock back effect
		if self.knock_back then
			-- direction error check
			dir = dir or vector_zero()

			local v = self.object:get_velocity()
			if not v then return end
			local rate = min(tflp, punch_interval) / punch_interval
			local kb = 1.25
			if tool_capabilities.damage_groups["knockback"] then
				kb = tool_capabilities.damage_groups["knockback"]
			end
			local luaentity = hitter and hitter:get_luaentity()
			if hitter and is_player then
				local wielditem = hitter:get_wielded_item()
				kb = kb + 3 * core.get_item_group(wielditem:get_name(), "hammer")
				-- add player velocity to mob knockback
				local dir_dot = (hv.x * dir.x) + (hv.z * dir.z)
				local player_mag = ((hv.x * hv.x) + (hv.z * hv.z))^0.5
				local mob_mag = ((v.x * v.x) + (v.z * v.z))^0.5
				if dir_dot > 0 and mob_mag <= player_mag * 0.625 then
					kb = kb + player_mag / 2 -- experimentally derived constant
				end
				kb = kb * rate
				kb = kb + 3 * mcl_enchanting.get_enchantment(wielditem, "knockback")
			elseif luaentity and luaentity._knockback and die == false then
				kb = kb + luaentity._knockback
			elseif luaentity and luaentity._knockback and die == true then
				kb = kb + luaentity._knockback * 0.25
			end
			if die then
				kb = kb * 1.25
				self.vl_drops_pos = mob_pos
			end

			local up = 5.25
			-- if already in air then dont go up anymore when hit
			if abs(v.y) > 0.1 or self.fly then up = 0 end

			self._kb_turn = true
			self:turn_by(HALFPI, .1) -- knockback turn
			self.frame_speed_multiplier=2.3
			if self.animation.run_end then
				self:set_animation("run")
			elseif self.animation.walk_end then
				self:set_animation("walk")
			end
			minetest.after(0.2, function()
				if self and self.object then
					self.frame_speed_multiplier=1
					self._kb_turn = false
				end
			end)
			kb = kb * 20 -- experimentally derived constant
			self:set_velocity(0)
			self.object:add_velocity(vector_new(dir.x * kb, up, dir.z * kb ))

			self.pause_timer = 0.25
		end
	end -- END if damage

	-- if skittish then run away
	if hitter and is_player and hitter:get_pos() and not die and self.runaway == true and self.state ~= "flop" then
		local hp, sp = hitter:get_pos(), self.object:get_pos()
		self:turn_in_direction(sp.x - hp.x, sp.z - hp.z, 1)
		minetest.after(0.2,function()
			if self and self.object and hitter and is_player then
				local hp, sp = hitter:get_pos(), self.object:get_pos()
				if hp and sp then
					self:turn_in_direction(sp.x - hp.x, sp.z - hp.z, 1)
					self:set_velocity(self.run_velocity)
				end
			end
		end)
		self.state = "runaway"
		self.runaway_timer = 0
		self.following = nil
	end

	local name = hitter:get_player_name() or ""

	-- attack puncher
	if self.passive == false
	and self.state ~= "flop"
	and (self.child == false or self.type == "monster")
	and hitter:get_player_name() ~= self.owner
	and not mcl_mobs.invis[ name ] then
		if not die then
			-- attack whoever punched mob
			self.state = ""
			self:do_attack(hitter)
			self._aggro= true
		end
	end

	-- alert others to the attack
	local alert_pos = hitter:get_pos()
	if alert_pos then
		local objs = minetest.get_objects_inside_radius(alert_pos, self.view_range)
		local obj = nil

		for n = 1, #objs do
			obj = objs[n]:get_luaentity()

			if obj then
				-- only alert members of same mob or friends
				if obj.group_attack
				and obj.state ~= "attack"
				and obj.owner ~= name then
					if obj.name == self.name then
						obj:do_attack(hitter)
					elseif type(obj.group_attack) == "table" then
						for i=1, #obj.group_attack do
							if obj.group_attack[i] == self.name then
								obj._aggro = true
								obj:do_attack(hitter)
								break
							end
						end
					end
				end

				-- have owned mobs attack player threat
				if obj.owner == name and obj.owner_loyal then
					obj:do_attack(self.object)
				end
			end
		end
	end
end

function mob_class:check_aggro(dtime)
	if not self._aggro or not self.attack then return end
	if not self._check_aggro_timer then self._check_aggro_timer = 0 end
	if self._check_aggro_timer > 5 then
		self._check_aggro_timer = 0

		if self.attack then
			-- TODO consider removing this in favour of what is done in do_states_attack
			-- Attack is dropped in do_states_attack if out of range, so won't even trigger here
			-- I do not think this code does anything. Are mobs still loaded in at 128?
			if not self.attack:get_pos() or vector_distance(self.attack:get_pos(),self.object:get_pos()) > 128 then
				self._aggro = nil
				self.attack = nil
				self.state = "stand"
			end
		end
	end
	self._check_aggro_timer = self._check_aggro_timer + dtime
end



local function clear_aggro(self)
	self.state = "stand"
	self:set_velocity( 0)
	self:set_animation( "stand")

	self.attack = nil
	self._aggro = nil

	self.v_start = false
	self.timer = 0
	self.blinktimer = 0
	self.path.way = nil
end

function mob_class:do_states_attack(dtime)
	self.timer = self.timer + dtime
	if self.timer > 100 then self.timer = 1 end

	local s = self.object:get_pos()
	if not s then return end

	local p = self.attack:get_pos() or s
	local yaw = self.object:get_yaw() or 0

	-- stop attacking if player invisible or out of range
	if not self.attack
			or not self.attack:get_pos()
			or not self:object_in_range(self.attack)
			or self.attack:get_hp() <= 0
			or (self.attack:is_player() and mcl_mobs.invis[ self.attack:get_player_name() ]) then

		clear_aggro(self)
		return
	end

	local target_line_of_sight = self:target_visible(s)

	if not target_line_of_sight then
		if self.target_time_lost then
			local time_since_seen = os.time() - self.target_time_lost
			if time_since_seen > TIME_TO_FORGET_TARGET then
				self.target_time_lost = nil
				clear_aggro(self)
				return
			end
		else
			self.target_time_lost = os.time()
		end
	else
		self.target_time_lost = nil
	end

	-- calculate distance from mob and enemy
	local dist = vector_distance(p, s)

	if self.attack_type == "explode" then
		if target_line_of_sight then
			self:turn_in_direction(p.x - s.x, p.z - s.z, 1)
		end

		local node_break_radius = self.explosion_radius or 1
		local entity_damage_radius = self.explosion_damage_radius or (node_break_radius * 2)

		-- start timer when in reach and line of sight
		if not self.v_start and dist <= self.reach and target_line_of_sight then
			self.v_start = true
			self.timer = 0
			self.blinktimer = 0
			self:mob_sound("fuse", nil, false)
			self:set_animation("fuse")

			-- stop timer if out of reach or direct line of sight
		elseif self.allow_fuse_reset and self.v_start
				and (dist >= self.explosiontimer_reset_radius or not target_line_of_sight) then
			self.v_start = false
			self.timer = 0
			self.blinktimer = 0
			self.blinkstatus = false
			self:remove_texture_mod("^[brighten")
		end

		-- walk right up to player unless the timer is active
		if self.v_start and (self.stop_to_explode or dist < self.reach) or not target_line_of_sight then
			self:set_velocity(0)
		else
			self:set_velocity(self.run_velocity)
		end

		if not self.v_start then
			if self.animation and self.animation.run_start then
				self:set_animation("run")
			else
				self:set_animation("walk")
			end
		end

		if self.v_start then
			self.timer = self.timer + dtime
			self.blinktimer = (self.blinktimer or 0) + dtime

			if self.blinktimer > 0.2 then
				self.blinktimer = 0
				if self.blinkstatus then
					self:remove_texture_mod("^[brighten")
				else
					self:add_texture_mod("^[brighten")
				end
				self.blinkstatus = not self.blinkstatus
			end

			if self.timer > self.explosion_timer then
				local pos = self.object:get_pos()

				if mobs_griefing and not minetest.is_protected(pos, "") then
					mcl_explosions.explode(mcl_util.get_object_center(self.object), self.explosion_strength, { drop_chance = 1.0 }, self.object)
				else
					minetest.sound_play(self.sounds.explode, {
						pos = pos,
						gain = 1.0,
						max_hear_distance = self.sounds.distance or 32
					}, true)
					self:entity_physics(pos,entity_damage_radius)
					mcl_mobs.effect(pos, 32, "mcl_particles_smoke.png", nil, nil, node_break_radius, 1, 0)
				end
				mcl_burning.extinguish(self.object)
				mcl_util.remove_entity(self)

				return true
			end
		end

	elseif self.attack_type == "dogfight"
			or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 2) and (dist >= self.avoid_distance or not self.shooter_avoid_enemy)
			or (self.attack_type == "dogshoot" and dist <= self.reach and self:dogswitch() == 0) then

		if self.fly and dist > self.reach then
			local p1, p2 = s, p
			local me_y, p_y = floor(p1.y), floor(p2.y + 1)
			local v = self.object:get_velocity()

			if self:flight_check( s) then
				if me_y < p_y then
					self.object:set_velocity(vector_new(v.x,  1 * self.walk_velocity, v.z))
				elseif me_y > p_y then
					self.object:set_velocity(vector_new(v.x, -1 * self.walk_velocity, v.z))
				end
			else
				if me_y < p_y then
					self.object:set_velocity(vector_new(v.x,  0.01, v.z))
				elseif me_y > p_y then
					self.object:set_velocity(vector_new(v.x, -0.01, v.z))
				end
			end
		end

		-- rnd: new movement direction
		if self.path.following and self.path.way and self.attack_type ~= "dogshoot" then
			-- no paths longer than 50
			if #self.path.way > 50 or dist < self.reach then
				self.path.following = false
				return
			end

			local p1 = self.path.way[1]
			if not p1 then
				self.path.following = false
				return
			end

			if abs(p1.x - s.x) + abs(p1.z - s.z) < 0.6 then
				-- reached waypoint, remove it from queue
				table.remove(self.path.way, 1)
			end

			-- set new temporary target
			p = vector_copy(p1)
		end

		self:turn_in_direction(p.x - s.x, p.z - s.z, 10)

		-- move towards enemy if beyond mob reach
		if dist > self.reach then
			-- path finding by rnd
			if enable_pathfinding and self.pathfinding then
				self:smart_mobs(s, p, dist, dtime)
			end

			if self:is_at_cliff_or_danger() then
				self:set_velocity(0)
				self:set_animation("stand")
				--self:turn_by(PI * (random() - 0.5), 10)
			else
				if self.path.stuck then
					self:set_velocity(self.walk_velocity)
				else
					self:set_velocity(self.run_velocity)
				end
				if self.animation and self.animation.run_start then
					self:set_animation("run")
				else
					self:set_animation("walk")
				end
			end
		else -- rnd: if inside reach range
			self.path.stuck = false
			self.path.stuck_timer = 0
			self.path.following = false -- not stuck anymore

			self:set_velocity( 0)

			local attack_frequency = self.attack_frequency or 1

			if self.timer > attack_frequency then
				self.timer = 0

				if not self.custom_attack then
					if self.double_melee_attack and random(1, 2) == 1 then
						self:set_animation("punch2")
					else
						self:set_animation("punch")
					end

					if self:line_of_sight(vector_offset(p, 0, .5, 0), vector_offset(s, 0, .5, 0)) == true then
						self:mob_sound("attack")

						-- punch player (or what player is attached to)
						local attached = self.attack:get_attach()
						if attached then
							self.attack = attached
						end
						self.attack:punch(self.object, 1.0, {
							full_punch_interval = 1.0,
							damage_groups = {fleshy = self.damage}
						}, nil)
						if self.dealt_effect then
							mcl_potions.give_effect_by_level(self.dealt_effect.name, self.attack,
								self.dealt_effect.level, self.dealt_effect.dur)
						end
					end
				else
					self.custom_attack(self, p)
				end
			end
		end

	elseif self.attack_type == "shoot"
			or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 1)
			or (self.attack_type == "dogshoot" and (dist > self.reach or dist < self.avoid_distance and self.shooter_avoid_enemy) and self:dogswitch() == 0) then
		local vec = vector_new(p.x - s.x, p.y - s.y - 1, p.z - s.z)
		local dist = (vec.x*vec.x + vec.y*vec.y + vec.z*vec.z)^0.5
		self:turn_in_direction(vec.x, vec.z, 10)

		if self.strafes then
			if not self.strafe_direction then self.strafe_direction = HALFPI end
			if random(40) == 1 then self.strafe_direction = self.strafe_direction * -1 end

			local dir = -atan2(p.x - s.x, p.z - s.z)
			self.acc = vector_new(-sin(dir + self.strafe_direction) * 0.8, 0, cos(dir + self.strafe_direction) * 0.8)
			--stay away from player so as to shoot them
			if self.avoid_distance and dist < self.avoid_distance and self.shooter_avoid_enemy then
				local f = 0.3 * (self.avoid_distance - dist) / self.avoid_distance
				self.acc.x, self.acc.z = self.acc.x - sin(dir) * f, self.acc.z + cos(dir) * f
			end
		else
			self:set_velocity(0)
			self:set_animation("stand")
		end

		local p = self.object:get_pos()
		local cb = self.initial_properties.collisionbox
		p.y = p.y + (cb[2] + cb[5]) * 0.5

		if self.shoot_interval and self.timer > self.shoot_interval and random(1, 100) <= 60
				and not minetest.raycast(vector_offset(p, 0, self.shoot_offset, 0), vector_offset(self.attack:get_pos(), 0, 1.5, 0), false, false):next() then
			self.timer = 0
			self:set_animation( "shoot")

			-- play shoot attack sound
			self:mob_sound("shoot_attack")

			-- Shoot arrow
			if minetest.registered_entities[self.arrow] then
				local arrow, ent
				local v = 1
				local corr = vector.new(vec.x, 0, vec.z)
				corr = corr / corr:length()
				p = p + self.shoot_pos.x * corr
				p.y = p.y + self.shoot_pos.y

				if not self.shoot_arrow then
					self.firing = true
					minetest.after(1, function()
						self.firing = false
					end)

					arrow = vl_projectile.create(self.arrow, {
						pos = p,
						owner = self,
					})
					ent = arrow:get_luaentity()
					v = ent.velocity or v
					ent.switch = 1

					-- important for mcl_shields
					ent._shooter = self.object
					ent._saved_shooter_pos = self.object:get_pos()
					if ent.homing then
						ent._target = self.attack
					end
				end

				-- offset makes shoot aim accurate
				vec.y = vec.y + self.shoot_offset
				vec.x, vec.y, vec.z = vec.x * (v / dist), vec.y * (v / dist), vec.z * (v / dist)
				if self.shoot_arrow then
					vec = vector.normalize(vec)
					self:shoot_arrow(p, vec)
				else
					arrow:set_velocity(vec)
				end
			end
		end
	elseif self.attack_type == "custom" and self.attack_state then
		self.attack_state(self, dtime)
	end

	if self.on_attack then self.on_attack(self, dtime) end
end
