game-antum/mods/mobs/mobf/fighting.lua
2016-08-08 08:38:45 -07:00

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)