1542 lines
45 KiB
Lua
1542 lines
45 KiB
Lua
-------------------------------------------------------------------------------
|
|
-- Mob Framework Mod by Sapier
|
|
--
|
|
-- You may copy, use, modify or do nearly anything except removing this
|
|
-- copyright notice.
|
|
-- And of course you are NOT allow to pretend you have written it.
|
|
--
|
|
--! @file fighting.lua
|
|
--! @brief component for fighting related features
|
|
--! @copyright Sapier
|
|
--! @author Sapier
|
|
--! @date 2012-08-09
|
|
--
|
|
--! @defgroup fighting Combat subcomponent
|
|
--! @brief Component handling all fighting
|
|
--! @ingroup framework_int
|
|
--! @{
|
|
-- Contact: sapier a t gmx net
|
|
-------------------------------------------------------------------------------
|
|
|
|
--! @class fighting
|
|
|
|
--! @brief factor added to mob melee combat range to get its maximum agression radius
|
|
MOBF_AGRESSION_FACTOR = 5
|
|
|
|
--!@}
|
|
|
|
mobf_assert_backtrace(not core.global_exists("fighting"))
|
|
--! @brief fighting class reference
|
|
fighting = {}
|
|
fighting.healdb = minetest.world_setting_get("fighting.healdb")
|
|
|
|
--! @brief user defined on death callback
|
|
--! @memberof fighting
|
|
fighting.on_death_callbacks = {}
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] register_on_death_callback(callback)
|
|
--
|
|
--! @brief register an additional callback to be called on death of a mob
|
|
--! @memberof fighting
|
|
--
|
|
--! @param callback function to call
|
|
--! @return true/false
|
|
-------------------------------------------------------------------------------
|
|
function fighting.register_on_death_callback(callback)
|
|
|
|
if type(callback) == "function" then
|
|
|
|
table.insert(fighting.on_death_callbacks,callback)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] do_on_death_callback(entity,hitter)
|
|
--
|
|
--! @brief call all registred on_death callbacks
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity to do callback for
|
|
--! @param hitter object doing last punch
|
|
-------------------------------------------------------------------------------
|
|
function fighting.do_on_death_callback(entity,hitter)
|
|
|
|
for i,v in ipairs(fighting.on_death_callbacks) do
|
|
v(entity.data.name,entity.getbasepos(),hitter)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] push_back(entity,player)
|
|
--
|
|
--! @brief move a mob backward if it's punched
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mobbeing punched
|
|
--! @param dir direction to push back
|
|
-------------------------------------------------------------------------------
|
|
function fighting.push_back(entity,dir)
|
|
--get some base information
|
|
local mob_pos = entity.object:getpos()
|
|
local mob_basepos = entity.getbasepos(entity)
|
|
local dir_rad = mobf_calc_yaw(dir.x,dir.z)
|
|
local posdelta = mobf_calc_vector_components(dir_rad,0.5)
|
|
|
|
--push back mob
|
|
local new_pos = {
|
|
x=mob_basepos.x + posdelta.x,
|
|
y=mob_basepos.y,
|
|
z=mob_basepos.z + posdelta.z
|
|
}
|
|
|
|
local pos_valid = environment.possible_pos(entity,new_pos)
|
|
new_pos.y = mob_pos.y
|
|
local line_of_sight = mobf_line_of_sight(mob_pos,new_pos)
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: trying to punch mob from " .. printpos(mob_pos)
|
|
.. " to ".. printpos(new_pos))
|
|
if pos_valid and line_of_sight then
|
|
dbg_mobf.fighting_lvl2("MOBF: punching back ")
|
|
entity.object:moveto(new_pos)
|
|
else
|
|
dbg_mobf.fighting_lvl2("MOBF: not punching mob: " .. dump(pos_valid) .. " " ..dump(line_of_sight))
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] dodamage(entity,attacker)
|
|
--
|
|
--! @brief cause damage to be done to entity
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob being hit
|
|
--! @param attacker player/object hitting the mob
|
|
--! @param kill_reason reason to log for killing
|
|
-------------------------------------------------------------------------------
|
|
function fighting.dodamage(entity,attacker, kill_reason)
|
|
local mob_pos = entity.object:getpos()
|
|
|
|
--update lifebar
|
|
mobf_lifebar.set(entity.lifebar,entity.object:get_hp()/entity.hp_max)
|
|
|
|
-- make it die
|
|
if entity.object:get_hp() < 0.5 then
|
|
|
|
mobf_lifebar.del(entity.lifebar)
|
|
|
|
local result = entity.data.generic.kill_result
|
|
if type(entity.data.generic.kill_result) == "function" then
|
|
result = entity.data.generic.kill_result(entity, attacker)
|
|
end
|
|
|
|
|
|
--call on kill callback and superseed normal on kill handling
|
|
if entity.data.generic.on_kill_callback == nil or
|
|
entity.data.generic.on_kill_callback(entity,attacker) == false
|
|
then
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(mob_pos,entity.data.sound.die);
|
|
end
|
|
|
|
if attacker:is_player() then
|
|
if type(result) == "table" then
|
|
for i=1,#result, 1 do
|
|
if attacker:get_inventory():room_for_item("main", result[i]) then
|
|
attacker:get_inventory():add_item("main", result[i])
|
|
end
|
|
end
|
|
else
|
|
if attacker:get_inventory():room_for_item("main", result) then
|
|
attacker:get_inventory():add_item("main", result)
|
|
end
|
|
end
|
|
else
|
|
--todo check if spawning a stack is possible
|
|
minetest.add_item(mob_pos,result)
|
|
end
|
|
spawning.remove(entity, kill_reason)
|
|
else
|
|
dbg_mobf.fighting_lvl2("MOBF: ".. entity.data.name
|
|
.. " custom on kill handler superseeds generic handling")
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] hit(entity,attacker)
|
|
--
|
|
--! @brief handler for mob beeing hit
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob being hit
|
|
--! @param attacker player/object hitting the mob
|
|
-------------------------------------------------------------------------------
|
|
function fighting.hit(entity,attacker)
|
|
mobf_assert_backtrace(entity ~= nil)
|
|
mobf_assert_backtrace(attacker ~= nil)
|
|
|
|
--execute user defined on_hit_callback
|
|
if entity.data.generic.on_hit_callback ~= nil and
|
|
entity.data.generic.on_hit_callback(entity,attacker) == true
|
|
then
|
|
dbg_mobf.fighting_lvl2("MOBF: ".. entity.data.name .. " custom on hit handler superseeds generic handling")
|
|
return
|
|
end
|
|
|
|
--get some base information
|
|
local mob_pos = entity.object:getpos()
|
|
local mob_basepos = entity.getbasepos(entity)
|
|
local targetpos = attacker:getpos()
|
|
local dir = mobf_get_direction(targetpos,mob_basepos)
|
|
|
|
|
|
--don't attack spawner
|
|
if entity.dynamic_data.spawning.spawner ~= nil and
|
|
attacker:is_player() then
|
|
local playername = attacker:get_player_name()
|
|
if entity.dynamic_data.spawning.spawner == playername then
|
|
if entity.dynamic_data.state.current ~= "combat" then
|
|
local tool = attacker:get_wielded_item()
|
|
-- rotation is only done if player punches using hand
|
|
if tool:get_name() == "" then
|
|
local current_yaw = graphics.getyaw(entity)
|
|
graphics.setyaw(entity, current_yaw + math.pi/4)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--play hit sound
|
|
if entity.data.sound ~= nil then
|
|
sound.play(mob_pos,entity.data.sound.hit);
|
|
end
|
|
|
|
if entity.data.combat ~= nil and
|
|
entity.data.combat.on_hit_overlay ~= nil then
|
|
mobf_assert_backtrace( entity.data.combat.on_hit_overlay.texture ~= nil)
|
|
mobf_assert_backtrace( entity.data.combat.on_hit_overlay.timer ~= nil)
|
|
|
|
if entity.dynamic_data.combat.old_textures == nil then
|
|
entity.dynamic_data.combat.old_textures = entity.textures
|
|
end
|
|
|
|
local new_props = {
|
|
textures = { entity.dynamic_data.combat.old_textures[1] .. "^" ..
|
|
entity.data.combat.on_hit_overlay.texture }
|
|
}
|
|
|
|
|
|
|
|
core.after(entity.data.combat.on_hit_overlay.timer,function()
|
|
local restore_probs = {
|
|
textures = entity.dynamic_data.combat.old_textures
|
|
}
|
|
entity.dynamic_data.combat.old_textures = nil
|
|
entity.object:set_properties(restore_probs)
|
|
end)
|
|
|
|
entity.object:set_properties(new_props)
|
|
end
|
|
|
|
--push mob back
|
|
fighting.push_back(entity,dir)
|
|
|
|
--cause damage to be evaluated
|
|
fighting.dodamage(entity, attacker, "killed")
|
|
|
|
--dbg_mobf.fighting_lvl2("MOBF: attack chance is ".. entity.data.combat.angryness)
|
|
-- fight back
|
|
if entity.data.combat ~= nil and
|
|
( entity.data.combat.can_fight or
|
|
(entity.data.combat.angryness ~= nil and entity.data.combat.angryness > 0)
|
|
|
|
) and
|
|
entity.object:get_hp() > (entity.data.generic.base_health/3) then
|
|
|
|
--face attacker
|
|
if entity.mode ~= "3d" then
|
|
graphics.setyaw(entity, mobf_calc_yaw(dir.x,dir.z)-math.pi)
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: mob with chance of fighting back attacked")
|
|
--either the mob hasn't been attacked by now or a new player joined fight
|
|
|
|
if math.random() < entity.data.combat.angryness then
|
|
fighting.set_target(entity,attacker)
|
|
end
|
|
else
|
|
--make non agressive animals run away
|
|
fighting.run_away(entity,dir,attacker)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] run_away(entity,dir_to_enemy,enemy)
|
|
--
|
|
--! @brief make a mob run away
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to run away
|
|
--! @param dir_to_enemy direction towards enemy
|
|
--! @param enemy the enemy to avoid
|
|
-------------------------------------------------------------------------------
|
|
function fighting.run_away(entity,dir_to_enemy,enemy)
|
|
local flee_state = mob_state.get_state_by_name(entity,"flee")
|
|
|
|
if flee_state == nil then
|
|
local new_state = mob_state.get_state_by_name(entity,"walking")
|
|
local dir_rad = mobf_calc_yaw(dir_to_enemy.x,dir_to_enemy.z)
|
|
local fleevelocity = mobf_calc_vector_components(dir_rad,
|
|
entity.data.movement.max_accel*2)
|
|
|
|
local current_accel = entity.object:getacceleration()
|
|
local current_velocity = entity.object:getvelocity()
|
|
|
|
mob_state.change_state(entity,new_state)
|
|
|
|
entity.object:setvelocity({x=0,y=current_velocity.y,z=0})
|
|
entity.object:setacceleration({
|
|
x=fleevelocity.x,
|
|
y=current_accel.y,
|
|
z=fleevelocity.z}
|
|
)
|
|
else
|
|
mob_state.change_state(entity,flee_state)
|
|
entity.dynamic_data.current_movement_gen.set_target(entity,enemy)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] identify_combat_state(entity,target,distance)
|
|
--
|
|
--! @brief identify combat state to use
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to find state for
|
|
--! @param distance distance to target
|
|
--
|
|
--! @return state to use
|
|
-------------------------------------------------------------------------------
|
|
function fighting.identify_combat_state(entity,distance)
|
|
|
|
local target = entity.dynamic_data.combat.target
|
|
mobf_assert_backtrace(entity ~= nil)
|
|
mobf_assert_backtrace(target ~= nil)
|
|
|
|
local combat_melee = mob_state.get_state_by_name(entity,"combat_melee")
|
|
local combat_distance = mob_state.get_state_by_name(entity,"combat_distance")
|
|
local combat_generic = mob_state.get_state_by_name(entity,"combat")
|
|
|
|
if distance == nil then
|
|
local mob_pos = entity.object:getpos()
|
|
local targetpos = target:getpos()
|
|
distance = mobf_calc_distance(mob_pos,targetpos)
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: Identify combat state, mob: " .. entity.data.name .. " distance: " .. distance)
|
|
|
|
if combat_melee ~= nil and
|
|
distance < entity.data.combat.melee.range then
|
|
return combat_melee
|
|
end
|
|
|
|
if combat_distance and
|
|
entity.data.combat.distance ~= nil and
|
|
distance < entity.data.combat.distance.range and
|
|
(entity.data.combat.distance.min_range == nil or
|
|
distance > entity.data.combat.distance.min_range) then
|
|
|
|
-- distance within mele range
|
|
return combat_distance
|
|
end
|
|
|
|
return combat_generic
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] switch_to_combat_state(entity,now,target)
|
|
--
|
|
--! @brief switch to combat state
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to switch state
|
|
--! @param now current time in seconds
|
|
--! @param target the target to attack
|
|
-------------------------------------------------------------------------------
|
|
function fighting.switch_to_combat_state(entity,now,target)
|
|
|
|
mobf_assert_backtrace(entity ~= nil)
|
|
|
|
--precheck
|
|
if target == nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: no target for combat state change specified")
|
|
return
|
|
end
|
|
|
|
--set attack target
|
|
entity.dynamic_data.combat.target = target
|
|
|
|
local current_state = entity.dynamic_data.state.current
|
|
|
|
mobf_assert_backtrace(current_state.state_mode ~= "combat")
|
|
|
|
local combat_state = fighting.identify_combat_state(entity)
|
|
if combat_state == nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: no special combat state")
|
|
return
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: switching to combat state")
|
|
|
|
--make sure state is locked
|
|
mob_state.lock(entity,true)
|
|
|
|
--backup dynamic movement data
|
|
local backup = {}
|
|
backup.movement = entity.dynamic_data.movement
|
|
backup.p_movement = entity.dynamic_data.p_movement
|
|
|
|
--create new movement data
|
|
entity.dynamic_data.movement = {}
|
|
entity.dynamic_data.p_movement = {}
|
|
|
|
backup.current_state = entity.dynamic_data.state.current
|
|
dbg_mobf.fighting_lvl2("MOBF: backing up state: " .. backup.current_state.name)
|
|
|
|
--switch state
|
|
mob_state.change_state(entity,combat_state)
|
|
|
|
--save old movement data to use on switching back
|
|
entity.dynamic_data.combat.movement_backup = backup
|
|
|
|
--make sure a fighting mob ain't teleporting to target
|
|
entity.dynamic_data.movement.teleportsupport = false
|
|
|
|
--make sure we do follow our target
|
|
entity.dynamic_data.movement.guardspawnpoint = false
|
|
|
|
--set target
|
|
entity.dynamic_data.current_movement_gen.set_target(entity,target)
|
|
|
|
-- play start_attack sound
|
|
if entity.data.sound ~= nil and
|
|
entity.data.sound.start_attack ~= nil then
|
|
sound.play(entity.object:getpos(),entity.data.sound.start_attack);
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] restore_previous_state(entity,now)
|
|
--
|
|
--! @brief restore default movement generator of mob
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to restore movement generator
|
|
--! @param now current time in seconds
|
|
-------------------------------------------------------------------------------
|
|
function fighting.restore_previous_state(entity,now)
|
|
|
|
--check if ther is anything we can restore
|
|
if entity.dynamic_data.combat.movement_backup ~= nil then
|
|
local backup = entity.dynamic_data.combat.movement_backup
|
|
|
|
mobf_assert_backtrace(backup.current_state ~= "combat")
|
|
|
|
if backup.current_state ~= nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: restore state: " .. backup.current_state.name)
|
|
mob_state.change_state(entity,backup.current_state)
|
|
else
|
|
minetest.log(LOGLEVEL_WARNING,"MOBF: unable to restore previous state switching to default")
|
|
mob_state.change_state(entity,mob_state.get_state_by_name(entity,"default"))
|
|
end
|
|
|
|
backup.current_state = nil
|
|
|
|
--restore old movement data
|
|
entity.dynamic_data.movement = backup.movement
|
|
entity.dynamic_data.p_movement = backup.p_movement
|
|
|
|
--don't restore old movement target if not valid anymore
|
|
if entity.dynamic_data.movement.target == nil or
|
|
(not mobf_is_pos(entity.dynamic_data.movement.target) and
|
|
entity.dynamic_data.movement.target:getpos() == nil) then
|
|
entity.dynamic_data.movement.target = nil
|
|
end
|
|
|
|
--make sure all remaining data is deleted
|
|
entity.dynamic_data.combat.movement_backup = nil
|
|
end
|
|
|
|
--make sure state is unlocked
|
|
mob_state.lock(entity,false)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] in_range(entity,now)
|
|
--
|
|
--! @brief check if mob is within range of target
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob
|
|
--! @param distance to target
|
|
-------------------------------------------------------------------------------
|
|
function fighting.in_range(entity,distance)
|
|
if (entity.data.combat.melee == nil or
|
|
distance > entity.data.combat.melee.range) and
|
|
(entity.data.combat.distance == nil or
|
|
distance > entity.data.combat.distance.range) then
|
|
|
|
if entity.data.combat.melee ~= nil or
|
|
entity.data.combat.distance ~= nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: distance="..distance)
|
|
|
|
if entity.data.combat.melee ~= nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: melee="..entity.data.combat.melee.range)
|
|
end
|
|
if entity.data.combat.distance ~= nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: distance="..entity.data.combat.distance.range)
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] combat(entity,now)
|
|
--
|
|
--! @brief periodic callback called to do mobs own combat related actions
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param now current time
|
|
--! @param dtime time fraction since last call
|
|
--
|
|
--! @return continue callback execution or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.combat(entity,now,dtime)
|
|
|
|
--handle self destruct mobs
|
|
if fighting.self_destruct_handler(entity,now) then
|
|
return false
|
|
end
|
|
|
|
--fight against generic enemy "sun"
|
|
if fighting.sun_damage_handler(entity,now) then
|
|
return false
|
|
end
|
|
|
|
if entity.dynamic_data.combat ~= nil and
|
|
entity.dynamic_data.combat.target ~= nil then
|
|
|
|
--check if target is still valid
|
|
if not entity.dynamic_data.combat.target:is_player() then
|
|
local target_entity = entity.dynamic_data.combat.target:get_luaentity()
|
|
local target_pos = entity.dynamic_data.combat.target:getpos()
|
|
|
|
--print("MOBF: target is not player checking if stil valid: "
|
|
-- .. dump(target_entity) .. " " .. dump(target_pos))
|
|
|
|
if target_entity == nil or
|
|
target_entity.data == nil or
|
|
target_pos == nil then
|
|
|
|
-- switch back to default movement gen
|
|
fighting.restore_previous_state(entity,now)
|
|
|
|
--there is no player by that name, stop attack
|
|
entity.dynamic_data.combat.target = nil
|
|
|
|
dbg_mobf.fighting_lvl1("MOBF: not a valid target: "
|
|
.. dump(entity.dynamic_data.combat.target))
|
|
return true
|
|
end
|
|
end
|
|
|
|
--make mob run away if only 1/3 of health is left
|
|
if entity.object:get_hp() < (entity.data.generic.base_health/3) then
|
|
local old_target = fighting.get_target(entity)
|
|
|
|
--restore state before attack
|
|
fighting.restore_previous_state(entity,now)
|
|
|
|
--stop attack
|
|
entity.dynamic_data.combat.target = nil
|
|
|
|
--make mob run away
|
|
if old_target ~= nil then
|
|
local dir = mobf_get_direction(old_target:getpos(),entity.object:getpos())
|
|
fighting.run_away(entity,dir,old_target)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local targetname =
|
|
fighting.get_target_name(entity.dynamic_data.combat.target)
|
|
|
|
|
|
dbg_mobf.fighting_lvl1("MOBF: attacking player: "
|
|
..targetname)
|
|
|
|
|
|
--calculate some basic data
|
|
local mob_pos = entity.object:getpos()
|
|
local targetpos = entity.dynamic_data.combat.target:getpos()
|
|
local distance = mobf_calc_distance(mob_pos,targetpos)
|
|
local dir = mobf_get_direction(targetpos,mob_pos)
|
|
local target = entity.dynamic_data.combat.target
|
|
|
|
--look towards target
|
|
if entity.mode == "3d" then
|
|
graphics.setyaw(entity, mobf_calc_yaw(dir.x,dir.z)+math.pi)
|
|
else
|
|
graphics.setyaw(entity, mobf_calc_yaw(dir.x,dir.z)-math.pi)
|
|
end
|
|
|
|
--initiate self destruct
|
|
fighting.self_destruct_trigger(entity,distance,now)
|
|
|
|
local range = entity.data.combat.melee.range * MOBF_AGRESSION_FACTOR
|
|
|
|
if entity.data.combat.distance ~= nil and
|
|
entity.data.combat.distance.range > range then
|
|
range = entity.data.combat.distance.range
|
|
end
|
|
|
|
--find out if attacker is next to mob
|
|
if distance > range then
|
|
dbg_mobf.fighting_lvl2("MOBF: " .. entity.data.name .. " player >"
|
|
.. targetname .. "< to far away " .. distance .. " > "
|
|
.. range
|
|
.. " stopping attack")
|
|
|
|
--switch back to default movement gen
|
|
fighting.restore_previous_state(entity,now)
|
|
|
|
--there is no player by that name, stop attack
|
|
entity.dynamic_data.combat.target = nil
|
|
return true
|
|
end
|
|
|
|
--check if state needs to be switched
|
|
local required_state = fighting.identify_combat_state(entity,distance)
|
|
|
|
if required_state ~= nil and
|
|
required_state.name ~= entity.dynamic_data.state.current then
|
|
mob_state.change_state(entity,required_state)
|
|
|
|
--reset current attack target as movement target after state switch
|
|
entity.dynamic_data.current_movement_gen.set_target(entity,target)
|
|
end
|
|
|
|
--is mob near enough for any attack attack?
|
|
if not fighting.in_range(entity,distance) then
|
|
if entity.dynamic_data.combat.reset_path_counter > 1.5 then
|
|
entity.dynamic_data.current_movement_gen.set_target(entity,target)
|
|
entity.dynamic_data.combat.reset_path_counter = 0
|
|
end
|
|
entity.dynamic_data.combat.reset_path_counter =
|
|
entity.dynamic_data.combat.reset_path_counter + dtime
|
|
return true
|
|
end
|
|
|
|
--check if melee attack can be done
|
|
if fighting.melee_attack_handler(entity,now,distance) == false then
|
|
--check if distance attack can be done
|
|
if fighting.distance_attack_handler(entity,
|
|
targetpos,mob_pos,now,distance) then
|
|
|
|
-- mob did an attack so give chance to stop attack
|
|
local rand_value = math.random()
|
|
|
|
if entity.data.combat.angryness ~= nil and
|
|
rand_value > entity.data.combat.angryness then
|
|
dbg_mobf.fighting_lvl2("MOBF: rand=".. rand_value
|
|
.. " angryness=" .. entity.data.combat.angryness)
|
|
dbg_mobf.fighting_lvl2("MOBF: " .. entity.data.name .. " "
|
|
.. now .. " random aborting attack at "
|
|
..targetname)
|
|
-- switch back to default movement gen
|
|
fighting.restore_previous_state(entity,now)
|
|
entity.dynamic_data.combat.target = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] get_target(entity)
|
|
--
|
|
--! @brief find and possible target next to mob
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to look around
|
|
--! @return target
|
|
-------------------------------------------------------------------------------
|
|
function fighting.get_target(entity)
|
|
|
|
local possible_targets = {}
|
|
|
|
if entity.data.combat.melee.range > 0 then
|
|
|
|
local range = entity.data.combat.melee.range*MOBF_AGRESSION_FACTOR
|
|
|
|
if entity.data.combat.distance ~= nil and
|
|
entity.data.combat.distance.range > range then
|
|
range = entity.data.combat.distance.range
|
|
end
|
|
|
|
local objectlist = minetest.get_objects_inside_radius(
|
|
entity.object:getpos(),range)
|
|
|
|
local count = 0
|
|
|
|
for i,v in ipairs(objectlist) do
|
|
|
|
if v:is_player() then
|
|
local playername = v:get_player_name()
|
|
|
|
--don't attack spawner
|
|
if entity.dynamic_data.spawning.spawner == nil or
|
|
entity.dynamic_data.spawning.spawner ~= playername then
|
|
count = count + 1
|
|
table.insert(possible_targets,v)
|
|
dbg_mobf.fighting_lvl2("MOBF: " .. playername ..
|
|
" is next to a mob of type ".. entity.data.name)
|
|
else
|
|
dbg_mobf.fighting_lvl2("MOBF: " .. entity.data.name ..
|
|
" not attacking: " .. playername .. " is spawner")
|
|
end
|
|
else
|
|
if entity.data.combat.attack_hostile_mobs then
|
|
dbg_mobf.fighting_lvl2("MOBF: " .. entity.data.name ..
|
|
" trying to attack hostile mobs too")
|
|
|
|
local target_entity = v:get_luaentity()
|
|
if target_entity ~= nil then
|
|
local same_origin_protection = false
|
|
|
|
if mobf_rtd.factions_available then
|
|
same_origin_protection = not attention.is_enemy(entity,v)
|
|
elseif target_entity.dynamic_data ~= nil and
|
|
target_entity.dynamic_data.spawning ~= nil then
|
|
|
|
same_origin_protection =
|
|
target_entity.dynamic_data.spawning.spawner ==
|
|
entity.dynamic_data.spawning.spawner
|
|
end
|
|
|
|
if target_entity ~= entity and
|
|
target_entity.data ~= nil and
|
|
target_entity.data.combat ~= nil and
|
|
target_entity.data.combat.starts_attack and
|
|
not same_origin_protection then
|
|
|
|
table.insert(possible_targets,v)
|
|
dbg_mobf.fighting_lvl3(target_entity.data.name
|
|
.. " is next to a mob of type "
|
|
.. entity.data.name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
dbg_mobf.fighting_lvl2("MOBF: found ".. count .. " objects within" ..
|
|
" attack range of " .. entity.data.name)
|
|
end
|
|
|
|
|
|
local targets_within_sight = {}
|
|
|
|
for i,v in ipairs(possible_targets) do
|
|
|
|
local entity_pos = entity.object:getpos()
|
|
local target_pos = v:getpos()
|
|
|
|
--is there a line of sight between mob and possible target
|
|
--line of sight is calculated 1block above ground
|
|
if mobf_line_of_sight({x=entity_pos.x,y=entity_pos.y+1,z=entity_pos.z},
|
|
{x=target_pos.x,y=target_pos.y+1,z=target_pos.z}) then
|
|
|
|
table.insert(targets_within_sight,v)
|
|
end
|
|
|
|
end
|
|
|
|
local nearest_target = nil
|
|
local min_distance = -1
|
|
|
|
for i,v in ipairs(targets_within_sight) do
|
|
|
|
local distance = mobf_calc_distance(entity.object:getpos(),v:getpos())
|
|
|
|
if min_distance < 0 or
|
|
distance < min_distance then
|
|
|
|
nearest_target = v
|
|
min_distance = distance
|
|
end
|
|
|
|
end
|
|
|
|
return nearest_target
|
|
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] self_destruct_trigger(entity,distance)
|
|
--
|
|
--! @brief handle self destruct features
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param distance current distance to target
|
|
--! @param now current time
|
|
--! @return true/false if handled or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.self_destruct_trigger(entity,distance,now)
|
|
if entity.data.combat ~= nil and
|
|
entity.data.combat.self_destruct ~= nil then
|
|
|
|
dbg_mobf.fighting_lvl1("MOBF: checking for self destruct trigger " ..
|
|
distance .. " " ..
|
|
entity.dynamic_data.combat.ts_self_destruct_triggered ..
|
|
" " .. now)
|
|
|
|
--trigger self destruct
|
|
if distance <= entity.data.combat.self_destruct.range and
|
|
entity.dynamic_data.combat.ts_self_destruct_triggered == -1 then
|
|
dbg_mobf.fighting_lvl2("MOBF: self destruct triggered")
|
|
entity.dynamic_data.combat.ts_self_destruct_triggered = now
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] do_area_damage(pos,immune,damage_groups,range)
|
|
--
|
|
--! @brief damage all objects within a certain range
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param pos cennter of damage area
|
|
--! @param immune object immune to damage
|
|
--! @param damage_groups list of damage groups to do damage to
|
|
--! @param range range around pos
|
|
-------------------------------------------------------------------------------
|
|
function fighting.do_area_damage(pos,immune,damage_groups,range)
|
|
--damage objects within inner blast radius
|
|
mobf_assert_backtrace(type(range) ~= "table")
|
|
|
|
local objs = minetest.get_objects_inside_radius(pos, range)
|
|
for k, obj in pairs(objs) do
|
|
|
|
--don't do damage to issuer
|
|
if obj ~= immune and obj ~= nil then
|
|
|
|
--TODO as long as minetest still crashes without puncher use this workaround
|
|
|
|
local worst_damage = 0
|
|
if type(damage_groups) == "table" then
|
|
for k,v in pairs(damage_groups) do
|
|
if v > worst_damage then
|
|
worst_damage = v
|
|
end
|
|
end
|
|
elseif type(damage_groups) == "number" then
|
|
worst_damage = damage_groups
|
|
else
|
|
mobf_assert_backtrace("invalid damage_groups" == "selected")
|
|
end
|
|
|
|
|
|
local current_hp = obj:get_hp()
|
|
obj:set_hp(current_hp - worst_damage)
|
|
|
|
--punch
|
|
--obj:punch(nil, 1.0, {
|
|
-- full_punch_interval=1.0,
|
|
-- damage_groups = damage_groups,
|
|
--}, nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] do_node_damage(pos,immune_list,range,chance)
|
|
--
|
|
--! @brief damage all objects within a certain range
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @brief damage all nodes within a certain range
|
|
--
|
|
--! @param pos center of area
|
|
--! @param immune_list list of nodes immune to damage
|
|
--! @param range range to do damage
|
|
--! @param chance chance damage is done to a node
|
|
-------------------------------------------------------------------------------
|
|
function fighting.do_node_damage(pos,immune_list,range,chance)
|
|
--do node damage
|
|
for i=pos.x-range, pos.x+range, 1 do
|
|
for j=pos.y-range, pos.y+range, 1 do
|
|
for k=pos.z-range,pos.z+range,1 do
|
|
--TODO create a little bit more sophisticated blast resistance
|
|
if math.random() < chance then
|
|
local toremove = minetest.get_node({x=i,y=j,z=k})
|
|
|
|
if toremove ~= nil then
|
|
local immune = false
|
|
|
|
if immune_list ~= nil then
|
|
for i,v in ipairs(immune_list) do
|
|
if (toremove.name == v) then
|
|
immune = true
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
if immune ~= true then
|
|
minetest.remove_node({x=i,y=j,z=k})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] self_destruct_handler(entity)
|
|
--
|
|
--! @brief handle self destruct features
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param now current time
|
|
--! @return true/false if handled or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.self_destruct_handler(entity,now)
|
|
--self destructing mob?
|
|
if entity.data.combat ~= nil and
|
|
entity.data.combat.self_destruct ~= nil then
|
|
|
|
local pos = entity.object:getpos()
|
|
|
|
dbg_mobf.fighting_lvl1("MOBF: checking for self destruct imminent")
|
|
--do self destruct
|
|
if entity.dynamic_data.combat.ts_self_destruct_triggered > 0 and
|
|
entity.dynamic_data.combat.ts_self_destruct_triggered +
|
|
entity.data.combat.self_destruct.delay
|
|
<= now then
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: executing self destruct")
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(pos,entity.data.sound.self_destruct);
|
|
end
|
|
|
|
fighting.do_area_damage(pos,nil,
|
|
entity.data.combat.self_destruct.damage,
|
|
entity.data.combat.self_destruct.range)
|
|
|
|
--TODO determine block removal by damage and remove blocks
|
|
fighting.do_node_damage(pos,{},
|
|
entity.data.combat.self_destruct.node_damage_range,
|
|
1 - 1/entity.data.combat.self_destruct.node_damage_range)
|
|
|
|
if mobf_rtd.fire_enabled then
|
|
--Add fire
|
|
for i=pos.x-entity.data.combat.self_destruct.range/2,
|
|
pos.x+entity.data.combat.self_destruct.range/2, 1 do
|
|
for j=pos.y-entity.data.combat.self_destruct.range/2,
|
|
pos.y+entity.data.combat.self_destruct.range/2, 1 do
|
|
for k=pos.z-entity.data.combat.self_destruct.range/2,
|
|
pos.z+entity.data.combat.self_destruct.range/2, 1 do
|
|
|
|
local current = minetest.get_node({x=i,y=j,z=k})
|
|
|
|
if (current.name == "air") then
|
|
minetest.set_node({x=i,y=j,z=k},
|
|
{name="fire:basic_flame"})
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
else
|
|
minetest.log(LOGLEVEL_NOTICE,
|
|
"MOBF: self destruct without fire isn't really impressive!")
|
|
end
|
|
mobf_lifebar.del(entity.lifebar)
|
|
spawning.remove(entity, "self destruct")
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] melee_attack_handler(entity,now)
|
|
--
|
|
--! @brief handle melee attack
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param now current time
|
|
--! @param distance distance to player
|
|
--! @return true/false if handled or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.melee_attack_handler(entity,now,distance)
|
|
|
|
if entity.data.combat.melee == nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: no meele attack specified")
|
|
return false
|
|
end
|
|
|
|
local time_of_next_attack_chance = entity.dynamic_data.combat.ts_last_attack
|
|
+ entity.data.combat.melee.speed
|
|
--check if mob is ready to attack
|
|
if now < time_of_next_attack_chance then
|
|
dbg_mobf.fighting_lvl1("MOBF: to early for meele attack " ..
|
|
now .. " >= " .. time_of_next_attack_chance)
|
|
return false
|
|
end
|
|
|
|
mobf_assert_backtrace( entity.dynamic_data.combat.target ~= nil)
|
|
local ownpos = entity.object:getpos()
|
|
local target_obj = entity.dynamic_data.combat.target.object
|
|
|
|
if target_obj == nil then
|
|
target_obj = entity.dynamic_data.combat.target
|
|
end
|
|
|
|
if distance <= entity.data.combat.melee.range
|
|
and mobf_line_of_sight(ownpos,target_obj:getpos()) then
|
|
|
|
--save time of attack
|
|
entity.dynamic_data.combat.ts_last_attack = now
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(entity.object:getpos(),entity.data.sound.melee);
|
|
end
|
|
|
|
--calculate damage to be done
|
|
local damage_done =
|
|
math.floor(math.random(0,entity.data.combat.melee.maxdamage)) + 1
|
|
|
|
--TODO call punch instead of manually setting health for player too
|
|
if target_obj:is_player() then
|
|
local target_health = target_obj:get_hp()
|
|
|
|
--do damage
|
|
target_obj:set_hp(target_health -damage_done)
|
|
else
|
|
local damage_groups = nil
|
|
|
|
if entity.data.combat.melee.weapon_groupcaps ~= nil then
|
|
damage_groups = entity.data.combat.melee.weapon_damage_groups
|
|
end
|
|
|
|
if damage_groups == nil and
|
|
entity.data.combat.melee.weapongroups ~= nil then
|
|
damage_groups = {}
|
|
|
|
for i=1, #entity.data.combat.melee.weapongroups, 1 do
|
|
damage_groups[entity.data.combat.melee.weapon_damage_groups[i]] = damage_done
|
|
end
|
|
end
|
|
|
|
if damage_groups == nil then
|
|
damage_groups= { fleshy=damage_done }
|
|
|
|
end
|
|
|
|
target_obj:punch(entity.object, 1.0, {
|
|
full_punch_interval=1.0,
|
|
damage_groups = damage_groups,
|
|
}, nil)
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: ".. entity.data.name ..
|
|
" doing melee attack damage=" .. damage_done)
|
|
|
|
return true
|
|
end
|
|
dbg_mobf.fighting_lvl1("MOBF: not within meele range " ..
|
|
distance .. " > " .. entity.data.combat.melee.range)
|
|
return false
|
|
end
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] distance_attack_handler(entity,now)
|
|
--
|
|
--! @brief handle distance attack
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param targetpos position of target
|
|
--! @param mob_pos position of mob
|
|
--! @param now current time
|
|
--! @param distance distance between target and player
|
|
--! @return true/false if handled or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.distance_attack_handler(entity,targetpos,mob_pos,now,distance)
|
|
if entity.data.combat.distance == nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: no distance attack specified")
|
|
return false
|
|
end
|
|
|
|
local time_of_next_attack_chance = entity.dynamic_data.combat.ts_last_attack
|
|
+ entity.data.combat.distance.speed
|
|
|
|
--check if mob is ready to attack
|
|
if now < time_of_next_attack_chance then
|
|
dbg_mobf.fighting_lvl1("MOBF: to early for distance attack " ..
|
|
now .. " >= " .. time_of_next_attack_chance)
|
|
return false
|
|
end
|
|
|
|
if distance <= entity.data.combat.distance.range and
|
|
(entity.data.combat.distance.min_range == nil or
|
|
distance > entity.data.combat.distance.min_range)
|
|
then
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: ".. entity.data.name ..
|
|
" doing distance attack")
|
|
|
|
--save time of attack
|
|
entity.dynamic_data.combat.ts_last_attack = now
|
|
|
|
local dir = mobf_get_direction({ x=mob_pos.x,
|
|
y=mob_pos.y+1,
|
|
z=mob_pos.z
|
|
},
|
|
targetpos)
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(mob_pos,entity.data.sound.distance);
|
|
end
|
|
|
|
local newobject=minetest.add_entity({ x=mob_pos.x+dir.x,
|
|
y=mob_pos.y+dir.y+1,
|
|
z=mob_pos.z+dir.z
|
|
},
|
|
entity.data.combat.distance.attack
|
|
)
|
|
|
|
local thrown_entity = mobf_find_entity(newobject)
|
|
|
|
if thrown_entity ~= nil then
|
|
local vel_thrown = {
|
|
x=dir.x*thrown_entity.velocity + math.random(0,0.05),
|
|
y=dir.y*thrown_entity.velocity,
|
|
z=dir.z*thrown_entity.velocity + math.random(0,0.05),
|
|
}
|
|
|
|
if entity.data.combat.distance.balistic == true then
|
|
--this isn't an exact calculation but just something to make
|
|
--it not too perfect
|
|
local height_diff = targetpos.y - mob_pos.y
|
|
local current_scalar_speed =
|
|
mobf_calc_scalar_speed(vel_thrown.x,vel_thrown.z)
|
|
local time_to_target = (distance/current_scalar_speed)
|
|
|
|
local y_vel = mobf_balistic_start_speed(
|
|
height_diff -1,
|
|
time_to_target,
|
|
-thrown_entity.gravity)
|
|
|
|
vel_thrown.y = y_vel + math.random(0,0.25)
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: throwing with velocity: " ..
|
|
printpos(vel_thrown))
|
|
|
|
newobject:setvelocity(vel_thrown)
|
|
|
|
newobject:setacceleration({x=0, y=-thrown_entity.gravity, z=0})
|
|
thrown_entity.owner = entity.object
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(mob_pos,entity.data.sound.shoot_distance);
|
|
end
|
|
|
|
dbg_mobf.fighting_lvl2("MOBF: distance attack issued")
|
|
else
|
|
minetest.log(LOGLEVEL_ERROR,
|
|
"MOBF: unable to find entity for distance attack")
|
|
end
|
|
return true
|
|
end
|
|
dbg_mobf.fighting_lvl1("MOBF: not within distance range " ..
|
|
distance .. " > " ..
|
|
entity.data.combat.distance.range)
|
|
return false
|
|
end
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] sun_damage_handler(entity,now)
|
|
--
|
|
--! @brief handle damage done by sun
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param now current time
|
|
--
|
|
--! @return true/false if killed or not
|
|
-------------------------------------------------------------------------------
|
|
function fighting.sun_damage_handler(entity,now)
|
|
if entity.data.combat ~= nil and
|
|
entity.data.combat.sun_sensitive then
|
|
mobf_assert_backtrace(entity.dynamic_data.combat ~= nil)
|
|
|
|
local pos = entity.object:getpos()
|
|
local current_state = entity.dynamic_data.state.current
|
|
local current_light = minetest.get_node_light(pos)
|
|
|
|
if current_light == nil then
|
|
mobf_bug_warning(LOGLEVEL_ERROR,"MOBF: Bug!!! didn't get a light value for "
|
|
.. printpos(pos))
|
|
return false
|
|
end
|
|
--check if mob is in sunlight
|
|
if ( current_light > LIGHT_MAX) then
|
|
dbg_mobf.fighting_lvl1("MOBF: " .. entity.data.name ..
|
|
" health at start:" .. entity.object:get_hp())
|
|
|
|
if current_state.animation ~= nil and
|
|
entity.data.animation ~= nil and
|
|
entity.data.animation[current_state.animation .. "__burning"] ~= nil then
|
|
graphics.set_animation(entity,current_state.animation .. "burning")
|
|
else
|
|
graphics.set_animation(entity,"burning")
|
|
end
|
|
|
|
|
|
if entity.dynamic_data.combat.ts_last_sun_damage +1 < now then
|
|
local damage = (1 + math.floor(entity.data.generic.base_health/15))
|
|
dbg_mobf.fighting_lvl1("Mob ".. entity.data.name .. " takes "
|
|
..damage .." damage because of sun")
|
|
|
|
entity.object:set_hp(entity.object:get_hp() - damage)
|
|
mobf_lifebar.set(entity.lifebar,entity.object:get_hp()/entity.hp_max)
|
|
|
|
if entity.data.sound ~= nil then
|
|
sound.play(pos,entity.data.sound.sun_damage);
|
|
end
|
|
|
|
if entity.object:get_hp() <= 0 then
|
|
--if entity.dynamic_data.generic.health <= 0 then
|
|
dbg_mobf.fighting_lvl2("Mob ".. entity.data.name .. " died of sun")
|
|
mobf_lifebar.del(entity.lifebar)
|
|
spawning.remove(entity,"died by sun")
|
|
return true
|
|
end
|
|
entity.dynamic_data.combat.ts_last_sun_damage = now
|
|
end
|
|
else
|
|
--use last sun damage to avoid setting animation over and over
|
|
--even if nothing changed
|
|
if entity.dynamic_data.combat.ts_last_sun_damage ~= -1 and
|
|
current_state.animation ~= nil then
|
|
graphics.set_animation(entity,current_state.animation)
|
|
entity.dynamic_data.combat.ts_last_sun_damage = -1
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] get_target_name(target)
|
|
--
|
|
--! @brief get name of target
|
|
--! @memberof fighting
|
|
--! @private
|
|
--
|
|
--! @param target to get name for
|
|
--
|
|
--! @return name of target
|
|
-------------------------------------------------------------------------------
|
|
function fighting.get_target_name(target)
|
|
if target == nil then
|
|
return "invalid"
|
|
end
|
|
|
|
if target:is_player() then
|
|
return target:get_player_name()
|
|
else
|
|
local target_entity = target:get_luaentity()
|
|
if target_entity ~= nil and
|
|
target_entity.data ~= nil and
|
|
target_entity.data.name ~= nil then
|
|
return "MOB: " .. target_entity.data.name
|
|
end
|
|
end
|
|
|
|
return "unknown"
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] set_target(entity,target)
|
|
--
|
|
--! @brief decide if only switching target or state
|
|
--! @memberof fighting
|
|
--! @public
|
|
--
|
|
--! @param entity entity to update
|
|
--! @param target to set
|
|
-------------------------------------------------------------------------------
|
|
function fighting.set_target(entity,target)
|
|
|
|
mobf_assert_backtrace(entity.dynamic_data ~= nil)
|
|
|
|
if not fighting.is_valid_target(target) then
|
|
return
|
|
end
|
|
|
|
if entity.dynamic_data.combat.target ~= nil then
|
|
dbg_mobf.fighting_lvl2("MOBF: switching attack target")
|
|
|
|
local target_distance = nil
|
|
if entity.data.combat.melee ~= nil and
|
|
entity.data.combat.melee.range ~= nil then
|
|
target_distance = 0.75 * entity.data.combat.melee.range
|
|
end
|
|
|
|
--set movement target
|
|
entity.dynamic_data.current_movement_gen.set_target(entity,
|
|
target, true, target_distance)
|
|
|
|
--set attack target
|
|
entity.dynamic_data.combat.target = target
|
|
else
|
|
if entity.dynamic_data.combat.target ~= target then
|
|
|
|
local attackername = fighting.get_target_name(target)
|
|
dbg_mobf.fighting_lvl2("MOBF: initial attack at: ".. attackername)
|
|
|
|
if entity.dynamic_data.combat.target == nil then
|
|
fighting.switch_to_combat_state(entity,mobf_get_current_time(),target)
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] is_valid_target(target)
|
|
--
|
|
--! @brief check if a target is a valid target
|
|
--! @memberof fighting
|
|
--! @public
|
|
--
|
|
--! @param target to set
|
|
-------------------------------------------------------------------------------
|
|
function fighting.is_valid_target(target)
|
|
|
|
--remove target case
|
|
if target == nil then
|
|
return true
|
|
end
|
|
|
|
--valid if it's a player
|
|
if target:is_player() then
|
|
return true
|
|
end
|
|
|
|
--valid if it's a lua entity
|
|
if target:get_luaentity() ~= nil then
|
|
return true
|
|
end
|
|
|
|
--invalid any other case
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] init_dynamic_data()
|
|
--
|
|
--! @brief initialize all dynamic data on activate
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param now current time
|
|
-------------------------------------------------------------------------------
|
|
function fighting.init_dynamic_data(entity,now)
|
|
local data = {
|
|
ts_last_sun_damage = now,
|
|
ts_last_attack = now,
|
|
ts_last_aggression_chance = now,
|
|
ts_self_destruct_triggered = -1,
|
|
|
|
target = nil,
|
|
reset_path_counter = 0,
|
|
}
|
|
|
|
entity.dynamic_data.combat = data
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] heal()
|
|
--
|
|
--! @brief heal a mob
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob to do action
|
|
--! @param player the one doing the rightclick
|
|
-------------------------------------------------------------------------------
|
|
function fighting.heal(entity,player)
|
|
|
|
local health = entity.object:get_hp()
|
|
|
|
if entity.data.generic.base_health == entity.object:get_hp() then
|
|
return
|
|
end
|
|
|
|
if not player:is_player() then
|
|
return
|
|
end
|
|
|
|
local tool = player:get_wielded_item()
|
|
|
|
if tool == nil then
|
|
return
|
|
end
|
|
|
|
tool = tool:get_name()
|
|
|
|
if not fighting.healdb or not fighting.healdb[tool] then
|
|
dbg_mobf.fighting_lvl1("MOBF: unknown heal item: " .. tool)
|
|
return
|
|
end
|
|
|
|
local new_health = 0
|
|
|
|
print("healdb value: " .. dump(fighting.healdb[tool].value))
|
|
|
|
if fighting.healdb[tool].value >= 0 then
|
|
new_health = MIN(entity.object:get_hp() + fighting.healdb[tool].value,
|
|
entity.data.generic.base_health)
|
|
else
|
|
new_health = MAX(entity.object:get_hp() + fighting.healdb[tool].value, 0)
|
|
end
|
|
|
|
entity.object:set_hp(new_health)
|
|
|
|
if new_health <= 0.5 then
|
|
fighting.dodamage(entity, player, "poisoned")
|
|
else
|
|
mobf_lifebar.set(entity.lifebar,new_health/entity.hp_max)
|
|
end
|
|
|
|
player:get_inventory():remove_item("main",tool.." 1")
|
|
|
|
if fighting.healdb[tool].replacement ~= nil then
|
|
player:get_inventory():add_item("main",
|
|
fighting.healdb[tool].replacement.." 1")
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] heal_caption()
|
|
--
|
|
--! @brief get caption for heal button
|
|
--! @memberof fighting
|
|
--
|
|
--! @param entity mob to do action
|
|
-------------------------------------------------------------------------------
|
|
function fighting.heal_caption(entity)
|
|
if entity.data.generic.base_health ~= entity.object:get_hp() then
|
|
return "heal"
|
|
else
|
|
return "nothing to heal"
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- @function [parent=#fighting] update_healdb()
|
|
--
|
|
--! @brief update mobfs heal db
|
|
--! @memberof fighting
|
|
--
|
|
--! @param hp_change
|
|
--! @param replace_with_item
|
|
--! @param itemstack -- unused
|
|
--! @param player player issuing the update
|
|
--! @param pointed_thing -- unused
|
|
-------------------------------------------------------------------------------
|
|
function fighting.update_healdb(hp_change, replace_with_item, itemstack, player,
|
|
pointed_thing)
|
|
|
|
if not player:is_player() then
|
|
return
|
|
end
|
|
|
|
local tool = player:get_wielded_item()
|
|
|
|
if tool == nil then
|
|
return
|
|
end
|
|
|
|
tool = tool:get_name()
|
|
|
|
local replacement = nil
|
|
if replace_with_item ~= nil then
|
|
if type(replace_with_item) ~= "string" then
|
|
replacement = replace_with_item:get_name()
|
|
end
|
|
end
|
|
|
|
if fighting.healdb == nil then
|
|
fighting.healdb = {}
|
|
end
|
|
|
|
if fighting.healdb[tool] ~= nil and
|
|
fighting.healdb[tool].value == hp_change and
|
|
fighting.healdb[tool].replacement == replacement or
|
|
hp_change == nil then
|
|
return false
|
|
end
|
|
|
|
|
|
fighting.healdb[tool] = {
|
|
value = hp_change,
|
|
replacement = replacement
|
|
}
|
|
|
|
minetest.world_setting_set("fighting.healdb",fighting.healdb)
|
|
return false
|
|
end
|
|
|
|
minetest.register_on_item_eat(fighting.update_healdb)
|