--= Creatures MOB-Engine (cme) =--
-- Copyright (c) 2015-2016 BlockMen <blockmen2015@gmail.com>
--
-- functions.lua
--
-- This software is provided 'as-is', without any express or implied warranty. In no
-- event will the authors be held liable for any damages arising from the use of
-- this software.
--
-- Permission is granted to anyone to use this software for any purpose, including
-- commercial applications, and to alter it and redistribute it freely, subject to the
-- following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
-- claim that you wrote the original software. If you use this software in a
-- product, an acknowledgment in the product documentation is required.
-- 2. Altered source versions must be plainly marked as such, and must not
-- be misrepresented as being the original software.
-- 3. This notice may not be removed or altered from any source distribution.
--


-- Localizations
local rnd = math.random


local function knockback(selfOrObject, dir, old_dir, strengh)
  local object = selfOrObject
  if selfOrObject.mob_name then
    object = selfOrObject.object
  end
  local current_fmd = object:get_properties().automatic_face_movement_dir or 0
  object:set_properties({automatic_face_movement_dir = false})
  object:setvelocity(vector.add(old_dir, {x = dir.x * strengh, y = 3.5, z = dir.z * strengh}))
  old_dir.y = 0
  core.after(0.4, function()
    object:set_properties({automatic_face_movement_dir = current_fmd})
    object:setvelocity(old_dir)
    selfOrObject.falltimer = nil
    if selfOrObject.stunned == true then
      selfOrObject.stunned = false
      if selfOrObject.can_panic == true then
        selfOrObject.target = nil
        selfOrObject.mode = "_run"
        selfOrObject.modetimer = 0
      end
    end
  end)
end

local function on_hit(me)
  core.after(0.1, function()
    me:settexturemod("^[colorize:#c4000099")
  end)
  core.after(0.5, function()
		me:settexturemod("")
	end)
end

local hasMoved = creatures.compare_pos

local function getDir(pos1, pos2)
  local retval
  if pos1 and pos2 then
    retval = {x = pos2.x - pos1.x, y = pos2.y - pos1.y, z = pos2.z - pos1.z}
  end
  return retval
end

local function getDistance(vec, fly_offset)
  if not vec then
    return -1
  end
  if fly_offset then
    vec.y = vec.y + fly_offset
  end
	return math.sqrt((vec.x)^2 + (vec.y)^2 + (vec.z)^2)
end

local findTarget = creatures.findTarget

local function update_animation(obj_ref, mode, anim_def)
  if anim_def and obj_ref then
    obj_ref:set_animation({x = anim_def.start, y = anim_def.stop}, anim_def.speed, 0, anim_def.loop)
  end
end

local function update_velocity(obj_ref, dir, speed, add)
  local velo = obj_ref:getvelocity()
  if not dir.y then
    dir.y = velo.y/speed
  end
  local new_velo = {x = dir.x * speed, y = dir.y * speed or velo.y , z = dir.z * speed}
  if add then
    new_velo = vector.add(velo, new_velo)
  end
  obj_ref:setvelocity(new_velo)
end

local function getYaw(dirOrYaw)
  local yaw = 360 * rnd()
  if dirOrYaw and type(dirOrYaw) == "table" then
    yaw = math.atan(dirOrYaw.z / dirOrYaw.x) + math.pi^2 - 2
    if dirOrYaw.x > 0 then
      yaw = yaw + math.pi
    end
  elseif dirOrYaw and type(dirOrYaw) == "number" then
    -- here could be a value based on given yaw
  end

  return yaw
end

local dropItems = creatures.dropItems

local function killMob(me, def)
  if not def then
    if me then
      me:remove()
    end
  end
  local pos = me:getpos()
  me:setvelocity(nullVec)
  me:set_properties({collisionbox = nullVec})
  me:set_hp(0)

  if def.sounds and def.sounds.on_death then
    local death_snd = def.sounds.on_death
    core.sound_play(death_snd.name, {pos = pos, max_hear_distance = death_snd.distance or 5, gain = death_snd.gain or 1})
  end

  if def.model.animations.death then
    local dur = def.model.animations.death.duration or 0.5
    update_animation(me, "death", def.model.animations["death"])
    core.after(dur, function()
      me:remove()
    end)
  else
    me:remove()
  end
  if def.drops then
    if type(def.drops) == "function" then
      def.drops(me:get_luaentity())
    else
      dropItems(pos, def.drops)
    end
  end
end

local function limit(value, min, max)
  if value < min then
    return min
  end
  if value > max then
    return max
  end
  return value
end

local function calcPunchDamage(obj, actual_interval, tool_caps)
  local damage = 0
  if not tool_caps or not actual_interval then
    return 0
  end
  local my_armor = obj:get_armor_groups() or {}
  for group,_ in pairs(tool_caps.damage_groups) do
    damage = damage + (tool_caps.damage_groups[group] or 0) * limit(actual_interval / tool_caps.full_punch_interval, 0.0, 1.0) * ((my_armor[group] or 0) / 100.0)
  end
  return damage or 0
end

local function onDamage(self, hp)
  local me = self.object
  local def = core.registered_entities[self.mob_name]
  hp = hp or me:get_hp()

  if hp <= 0 then
    self.stunned = true
      killMob(me, def)
  else
    on_hit(me) -- red flashing
    if def.sounds and def.sounds.on_damage then
      local dmg_snd = def.sounds.on_damage
      core.sound_play(dmg_snd.name, {pos = me:getpos(), max_hear_distance = dmg_snd.distance or 5, gain = dmg_snd.gain or 1})
    end
  end
end

local function changeHP(self, value)
  local me = self.object
  local hp = me:get_hp()
  hp = hp + math.floor(value)
  me:set_hp(hp)
  if value < 0 then
    onDamage(self, hp)
  end
end

local function checkWielded(wielded, itemList)
  for s,w in pairs(itemList) do
    if w == wielded then
      return true
    end
  end
  return false
end

local tool_uses = {0, 30, 110, 150, 280, 300, 500, 1000}
local function addWearout(player, tool_def)
	if not core.setting_getbool("creative_mode") then
		local item = player:get_wielded_item()
		if tool_def and tool_def.damage_groups and tool_def.damage_groups.fleshy then
			local uses = tool_uses[tool_def.damage_groups.fleshy] or 0
			if uses > 0 then
				local wear = 65535/uses
				item:add_wear(wear)
				player:set_wielded_item(item)
			end
		end
	end
end

local function spawnParticles(...)
end
if core.setting_getbool("creatures_enable_particles") == true then
  spawnParticles = function(pos, velocity, texture_str)
    local vel = vector.multiply(velocity, 0.5)
    vel.y = 0
    core.add_particlespawner({
      amount = 8,
      time = 1,
      minpos = vector.add(pos, -0.7),
      maxpos = vector.add(pos, 0.7),
      minvel = vector.add(vel, {x = -0.1, y = -0.01, z = -0.1}),
      maxvel = vector.add(vel, {x = 0.1,  y = 0,  z = 0.1}),
      minacc = vector.new(),
      maxacc = vector.new(),
      minexptime = 0.8,
      maxexptime = 1,
      minsize = 1,
      maxsize = 2.5,
      texture = texture_str,
    })
  end
end



-- --
-- Default entity functions
-- --

creatures.on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
  if self.stunned == true then
    return
  end

  local me = self.object
  local mypos = me:getpos()

  changeHP(self, calcPunchDamage(me, time_from_last_punch, tool_capabilities) * -1)
  if puncher then
    if self.hostile then
      self.mode = "attack"
      self.target = puncher
    end
    if time_from_last_punch >= 0.45 and self.stunned == false then
      if self.has_kockback == true then
        local v = me:getvelocity()
        v.y = 0
        if not self.can_fly then
          me:setacceleration({x = 0, y = -15, z = 0})
        end
        knockback(self, dir, v, 5)
        self.stunned = true
      end

      -- add wearout to weapons/tools
      addWearout(puncher, tool_capabilities)
    end
  end
end

creatures.on_rightclick = function(self, clicker)
end

creatures.on_step = function(self, dtime)
  -- first get the relevant specs; exit if we don't know anything (1-3ms)
  local def = core.registered_entities[self.mob_name]
  if not def then
    throw_error("Can't load creature-definition")
    return
  end

  -- timer updates
  self.lifetimer = self.lifetimer + dtime
  self.modetimer = self.modetimer + dtime
  self.soundtimer = self.soundtimer + dtime
  self.yawtimer = self.yawtimer + dtime
  self.nodetimer = self.nodetimer + dtime
  self.followtimer = self.followtimer + dtime
  if self.envtimer then
    self.envtimer = self.envtimer + dtime
  end
  if self.falltimer then
    self.falltimer = self.falltimer + dtime
  end
  if self.searchtimer then
    self.searchtimer = self.searchtimer + dtime
  end
  if self.attacktimer then
    self.attacktimer = self.attacktimer + dtime
  end
  if self.swimtimer then
    self.swimtimer = self.swimtimer + dtime
  end

  -- main
  if self.stunned == true then
    return
  end

  if self.lifetimer > def.stats.lifetime and not (self.mode == "attack" and self.target) then
    self.lifetimer = 0
    if not self.tamed or (self.tamed and def.stats.dies_when_tamed) then
      killMob(self.object, def)
    end
  end


  -- localize some things
  local modes = def.modes
  local current_mode = self.mode
  local me = self.object
  local current_pos = me:getpos()
  current_pos.y = current_pos.y + 0.5
  local moved = hasMoved(current_pos, self.last_pos) or false
  local fallen = false

  -- Update pos and current node if necessary
  if moved == true or not self.last_pos then
    -- for falldamage
    if self.has_falldamage and self.last_pos and not self.in_water then
      local dist = math.abs(current_pos.y - self.last_pos.y)
      if dist > 0 then
        self.fall_dist = self.fall_dist - dist
        if not self.falltimer then
          self.falltimer = 0
        end
      end
    end

    self.last_pos = current_pos
    if self.nodetimer > 0.2 then
      self.nodetimer = 0
      local current_node = core.get_node_or_nil(current_pos)
      self.last_node = current_node
      if def.stats.light then
        local wtime = core.get_timeofday()
        local llvl = core.get_node_light({x = current_pos.x, y = current_pos.y + 0.5, z = current_pos.z}) or 0
        self.last_llvl = llvl
      end
    end
  else
    if (modes[current_mode].moving_speed or 0) > 0 then
      update_velocity(me, nullVec, 0)
      if modes["idle"] and not (current_mode == "attack" or current_mode == "follow") then
        current_mode = "idle"
        self.modetimer = 0
      end
    end
    if self.fall_dist < 0 then
      fallen = true
    end
  end

  if fallen then
    local falltime = tonumber(self.falltimer) or 0
    local dist = math.abs(self.fall_dist) or 0
    self.falltimer = 0
    self.fall_dist = 0
    fallen = false

    local damage = 0
    if dist > 3 and not self.in_water and falltime/dist < 0.2 then
      damage = dist - 3
    end

    -- damage by calced value
    if damage > 0 then
      changeHP(self, damage * -1)
    end
  end


  -- special mode handling
  -- check distance to target
  if self.target and self.followtimer > 0.6 then
    self.followtimer = 0
    local p2 = self.target:getpos()
    local dir = getDir(current_pos, p2)
    local offset
    if self.can_fly then
      offset = modes["fly"].target_offset
    end
    local dist = getDistance(dir, offset)
    local radius
    if self.hostile and def.combat then
      radius = def.combat.search_radius
    elseif modes["follow"] then
      radius = modes["follow"].radius
    end
    if dist == -1 or dist > (radius or 5) then
      self.target = nil
      current_mode = ""
    elseif dist > -1 and self.hostile and dist < def.combat.attack_radius then
      -- attack
      if self.attacktimer > def.combat.attack_speed then
        self.attacktimer = 0
        if core.line_of_sight(current_pos, p2) == true then
          self.target:punch(me, 1.0,  {
            full_punch_interval = def.combat.attack_speed,
            damage_groups = {fleshy = def.combat.attack_damage}
          })
	      end
        update_velocity(me, self.dir, 0)
      end
    else
      if current_mode == "attack" or current_mode == "follow" then
        self.dir = vector.normalize(dir)
        me:setyaw(getYaw(dir))
        if self.in_water then
          self.dir.y = me:getvelocity().y
        end
        update_velocity(me, self.dir, modes[current_mode].moving_speed or 0)
      end
    end
  end

  -- search a target (1-2ms)
  if not self.target and ((self.hostile and def.combat.search_enemy) or modes["follow"]) and current_mode ~= "_run" then
    local timer
    if self.hostile then
      timer = def.combat.search_timer or 2
    elseif modes["follow"] then
      timer = modes["follow"].timer
    end
    if self.searchtimer > (timer or 4) then
      self.searchtimer = 0
      local targets = {}
      if self.hostile then
        targets = findTarget(me, current_pos, def.combat.search_radius, def.combat.search_type, def.combat.search_xray)
      else
        targets = findTarget(me, current_pos, modes["follow"].radius or 5, "player")
      end
      if #targets > 1 then
        self.target = targets[rnd(1, #targets)]
      elseif #targets == 1 then
        self.target = targets[1]
      end
      if self.target then
        if self.hostile and modes["attack"] then
          current_mode = "attack"
        else
          local name = self.target:get_wielded_item():get_name()
          if name and checkWielded(name, modes["follow"].items) == true then
            current_mode = "follow"
            self.modetimer = 0
          else
            self.target = nil
          end
        end
      end
    end
  end

  if current_mode == "eat" and not self.eat_node then
    local nodes = modes[current_mode].nodes
    local p = {x = current_pos.x, y = current_pos.y - 1, z = current_pos.z}
    local sn = core.get_node_or_nil(p)
    local eat_node
    for _,name in pairs(nodes) do
      if self.last_node ~= nil and name == self.last_node.name then
        eat_node = current_pos
        break
      elseif sn and sn.name == name then
        eat_node = p
        break
      end
    end

    if not eat_node then
      current_mode = "idle"
    else
      self.eat_node = eat_node
    end
  end


  -- further mode handling
  -- update mode
  if current_mode ~= "attack" and
      (current_mode == "" or self.modetimer > (modes[current_mode].duration or 4)) then
    self.modetimer = 0

    local new_mode = creatures.rnd(modes) or "idle"
    if new_mode == "eat" and self.in_water == true then
      new_mode = "idle"
    end
    if current_mode == "follow" and rnd(1, 10) < 3 then
      new_mode = current_mode
    elseif current_mode == "follow" then
      -- "lock" searching a little bit
      self.searchtimer = rnd(5, 8) * -1
      self.target = nil
    end
    current_mode = new_mode

    -- change eaten node when mode changes
    if self.eat_node then
      local n = core.get_node_or_nil(self.eat_node)
      local nnn = n.name
      local def = core.registered_nodes[n.name]
      local sounds
      if def then
         if def.drop and type(def.drop) == "string" then
           nnn = def.drop
         elseif not def.walkable then
           nnn = "air"
         end
      end
      if nnn and nnn ~= n.name and core.registered_nodes[nnn] then
        core.set_node(self.eat_node, {name = nnn})
        if not sounds then
          sounds = def.sounds
        end
        if sounds and sounds.dug then
          core.sound_play(sounds.dug, {pos = self.eat_node, max_hear_distance = 5, gain = 1})
        end
      end
      self.eat_node = nil
    end
  end

  -- mode has changes, do things
  if current_mode ~= self.last_mode then
    self.last_mode = current_mode

    local moving_speed = modes[current_mode].moving_speed or 0
    if moving_speed > 0 then
      local yaw = (getYaw(me:getyaw()) + 90.0) * DEGTORAD
      me:setyaw(yaw + 4.73)
      self.dir = {x = math.cos(yaw), y = 0, z = math.sin(yaw)}
      if self.can_fly then
        if current_pos.y >= (modes["fly"].max_height or 50) and not self.target then
          self.dir.y = -0.5
        else
          self.dir.y = (rnd() - 0.5)
        end
      end

      -- reduce speed in water
      if self.in_water == true then
        moving_speed = moving_speed * 0.7
      end
    else
        self.dir = nullVec
    end

    update_velocity(me, self.dir, moving_speed)
    local anim_def = def.model.animations[current_mode]
    if self.in_water and def.model.animations["swim"] then
      anim_def = def.model.animations["swim"]
    end
    update_animation(me, current_mode, anim_def)
  end

  -- update yaw
  if current_mode ~= "attack" and current_mode ~= "follow" and
      (modes[current_mode].update_yaw or 0) > 0 and
      self.yawtimer > (modes[current_mode].update_yaw or 4) then
    self.yawtimer = 0
    local mod = nil
    if current_mode == "_run" then
      mod = me:getyaw()
    end
    local yaw = (getYaw(mod) + 90.0) * DEGTORAD
    me:setyaw(yaw + 4.73)
    local moving_speed = modes[current_mode].moving_speed or 0
    if moving_speed > 0 then
      self.dir = {x = math.cos(yaw), y = nil, z = math.sin(yaw)}
      update_velocity(me, self.dir, moving_speed)
    end
  end

  --swim
  if self.can_swim and self.swimtimer > 0.8 and self.last_node then
    self.swimtimer = 0
    local name = self.last_node.name
    if name then
      if name == "default:water_source" then
        self.air_cnt = 0
        local vel = me:getvelocity()
        update_velocity(me, {x = vel.x, y = 0.9, z = vel.z}, 1)
        me:setacceleration({x = 0, y = -1.2, z = 0})
        self.in_water = true
        -- play swimming sounds
        if def.sounds and def.sounds.swim then
          local swim_snd = def.sounds.swim
          core.sound_play(swim_snd.name, {pos = current_pos, gain = swim_snd.gain or 1, max_hear_distance = swim_snd.distance or 10})
        end
        spawnParticles(current_pos, vel, "bubble.png")
      else
        self.air_cnt = self.air_cnt + 1
        if self.in_water == true and self.air_cnt > 5 then
          self.in_water = false
          if not self.can_fly then
            me:setacceleration({x = 0, y = -15, z = 0})
          end
        end
      end
    end
  end

  -- Add damage when drowning or in lava
  if self.env_damage and self.envtimer > 1 and self.last_node then
    self.envtimer = 0
    local name = self.last_node.name
    if not self.can_swim and name == "default:water_source" then
      changeHP(self, -1)
    elseif self.can_burn then
      if name == "fire:basic_flame" or name == "default:lava_source" then
        changeHP(self, -4)
      end
    end

    -- add damage when light is too bright or too dark
    local tod = core.get_timeofday() * 24000
    if self.last_llvl and self.can_burn and self.last_llvl > (def.stats.light.max or 15) and tod < 18000 then
      changeHP(self, -1)
    elseif self.last_llvl and self.last_llvl < (def.stats.light.min or 0) then
      changeHP(self, -2)
    end
  end

  -- Random sounds
  if def.sounds and def.sounds.random[current_mode] then
    local rnd_sound = def.sounds.random[current_mode]
    if not self.snd_rnd_time then
      self.snd_rnd_time = rnd((rnd_sound.time_min or 5), (rnd_sound.time_max or 35))
    end
    if rnd_sound and self.soundtimer > self.snd_rnd_time + rnd() then
      self.soundtimer = 0
      self.snd_rnd_time = nil
      core.sound_play(rnd_sound.name, {pos = current_pos, gain = rnd_sound.gain or 1, max_hear_distance = rnd_sound.distance or 30})
    end
  end

  self.mode = current_mode
end


creatures.get_staticdata = function(self)
  return {
    hp = self.object:get_hp(),
		mode = self.mode,
    tamed = self.tamed,
    modetimer = self.modetimer,
    lifetimer = self.lifetimer,
    soundtimer = self.soundtimer,
		fall_dist = self.fall_dist,
    in_water = self.in_water,
	}
end
