local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs local mob_class = mcl_mobs.mob_class local damage_enabled = minetest.settings:get_bool("enable_damage") local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false local show_health = false -- pathfinding settings local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching local stuck_path_timeout = 10 -- how long will mob follow path before giving up -- check if daytime and also if mob is docile during daylight hours function mob_class:day_docile() if self.docile_by_day == false then return false elseif self.docile_by_day == true and self.time_of_day > 0.2 and self.time_of_day < 0.8 then return true end end -- attack player/mob function mob_class:do_attack(player) if self.state == "attack" or self.state == "die" then return end self.attack = player self.state = "attack" -- TODO: Implement war_cry sound without being annoying --if random(0, 100) < 90 then --self:mob_sound("war_cry", true) --end end -- blast damage to entities nearby local function entity_physics(pos,radius) radius = radius * 2 local objs = minetest.get_objects_inside_radius(pos, radius) local obj_pos, dist for n = 1, #objs do obj_pos = objs[n]:get_pos() dist = vector.distance(pos, obj_pos) if dist < 1 then dist = 1 end local damage = math.floor((4 / dist) * radius) local ent = objs[n]:get_luaentity() -- punches work on entities AND players objs[n]:punch(objs[n], 1.0, { full_punch_interval = 1.0, damage_groups = {fleshy = damage}, }, pos) end end function mob_class:entity_physics(self,pos,radius) return entity_physics(pos,radius) end local los_switcher = false local height_switcher = false -- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3 function mob_class:smart_mobs(s, p, dist, dtime) local s1 = self.path.lastpos local target_pos = self.attack:get_pos() -- is it becoming stuck? if math.abs(s1.x - s.x) + math.abs(s1.z - s.z) < .5 then self.path.stuck_timer = self.path.stuck_timer + dtime else self.path.stuck_timer = 0 end self.path.lastpos = {x = s.x, y = s.y, z = s.z} local use_pathfind = false local has_lineofsight = minetest.line_of_sight( {x = s.x, y = (s.y) + .5, z = s.z}, {x = target_pos.x, y = (target_pos.y) + 1.5, z = target_pos.z}, .2) -- im stuck, search for path if not has_lineofsight then if los_switcher == true then use_pathfind = true los_switcher = false end -- cannot see target! else if los_switcher == false then los_switcher = true use_pathfind = false minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end -- can see target! end if (self.path.stuck_timer > stuck_timeout and not self.path.following) then use_pathfind = true self.path.stuck_timer = 0 minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then use_pathfind = true self.path.stuck_timer = 0 minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end if math.abs(vector.subtract(s,target_pos).y) > self.stepheight then if height_switcher then use_pathfind = true height_switcher = false end else if not height_switcher then use_pathfind = false height_switcher = true end end if use_pathfind then -- lets try find a path, first take care of positions -- since pathfinder is very sensitive local sheight = self.collisionbox[5] - self.collisionbox[2] -- round position to center of node to avoid stuck in walls -- also adjust height for player models! s.x = math.floor(s.x + 0.5) s.z = math.floor(s.z + 0.5) local ssight, sground = minetest.line_of_sight(s, { x = s.x, y = s.y - 4, z = s.z}, 1) -- determine node above ground if not ssight then s.y = sground.y + 1 end local p1 = self.attack:get_pos() p1.x = math.floor(p1.x + 0.5) p1.y = math.floor(p1.y + 0.5) p1.z = math.floor(p1.z + 0.5) local dropheight = 12 if self.fear_height ~= 0 then dropheight = self.fear_height end local jumpheight = 0 if self.jump and self.jump_height >= 4 then jumpheight = math.min(math.ceil(self.jump_height / 4), 4) elseif self.stepheight > 0.5 then jumpheight = 1 end self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch") self.state = "" do_attack(self, self.attack) -- no path found, try something else if not self.path.way then self.path.following = false -- lets make way by digging/building if not accessible if self.pathfinding == 2 and mobs_griefing then -- is player higher than mob? if s.y < p1.y then -- build upwards if not minetest.is_protected(s, "") then local ndef1 = minetest.registered_nodes[self.standing_in] if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then minetest.set_node(s, {name = mcl_mobs.fallback_node}) end end local sheight = math.ceil(self.collisionbox[5]) + 1 -- assume mob is 2 blocks high so it digs above its head s.y = s.y + sheight -- remove one block above to make room to jump if not minetest.is_protected(s, "") then local node1 = node_ok(s, "air").name local ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.set_node(s, {name = "air"}) minetest.add_item(s, ItemStack(node1)) end end s.y = s.y - sheight self.object:set_pos({x = s.x, y = s.y + 2, z = s.z}) else -- dig 2 blocks to make door toward player direction local yaw1 = self.object:get_yaw() + math.pi / 2 local p1 = { x = s.x + math.cos(yaw1), y = s.y, z = s.z + math.sin(yaw1) } if not minetest.is_protected(p1, "") then local node1 = node_ok(p1, "air").name local ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.add_item(p1, ItemStack(node1)) minetest.set_node(p1, {name = "air"}) end p1.y = p1.y + 1 node1 = node_ok(p1, "air").name ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.add_item(p1, ItemStack(node1)) minetest.set_node(p1, {name = "air"}) end end end end -- will try again in 2 seconds self.path.stuck_timer = stuck_timeout - 2 elseif s.y < p1.y and (not self.fly) then do_jump(self) --add jump to pathfinding self.path.following = true -- Yay, I found path! -- TODO: Implement war_cry sound without being annoying --self:mob_sound("war_cry", true) else self:set_velocity(self.walk_velocity) -- follow path now that it has it self.path.following = true end end end -- specific attacks local specific_attack = function(list, what) -- no list so attack default (player, animals etc.) if list == nil then return true end -- found entity on list to attack? for no = 1, #list do if list[no] == what then return true end end return false end -- find someone to attack function mob_class:monster_attack() if not damage_enabled or self.passive ~= false or self.state == "attack" or self:day_docile() then return end local s = self.object:get_pos() local p, sp, dist local player, obj, min_player local type, name = "", "" local min_dist = self.view_range + 1 local objs = minetest.get_objects_inside_radius(s, self.view_range) local blacklist_attack = {} for n = 1, #objs do if not objs[n]:is_player() then obj = objs[n]:get_luaentity() if obj then player = obj.object name = obj.name or "" end if obj and obj.type == self.type and obj.passive == false and obj.state == "attack" and obj.attack then table.insert(blacklist_attack, obj.attack) end end end for n = 1, #objs do if objs[n]:is_player() then if mcl_mobs.invis[ objs[n]:get_player_name() ] or (not self:object_in_range(objs[n])) then type = "" elseif (self.type == "monster" or self._aggro) then player = objs[n] type = "player" name = "player" end else obj = objs[n]:get_luaentity() if obj then player = obj.object type = obj.type name = obj.name or "" end end -- find specific mob to attack, failing that attack player/npc/animal if specific_attack(self.specific_attack, name) and (type == "player" or ( type == "npc" and self.attack_npcs ) or (type == "animal" and self.attack_animals == true)) then p = player:get_pos() sp = s dist = vector.distance(p, s) -- aim higher to make looking up hills more realistic p.y = p.y + 1 sp.y = sp.y + 1 local attacked_p = false for c=1, #blacklist_attack do if blacklist_attack[c] == player then attacked_p = true end end -- choose closest player to attack if dist < min_dist and not attacked_p and self:line_of_sight( sp, p, 2) == true then min_dist = dist min_player = player end end end if not min_player and #blacklist_attack > 0 then min_player=blacklist_attack[math.random(#blacklist_attack)] end -- attack player if min_player then self:do_attack(min_player) end end -- npc, find closest monster to attack function mob_class:npc_attack() if self.type ~= "npc" or not self.attacks_monsters or self.state == "attack" then return end local p, sp, obj, min_player local s = self.object:get_pos() local min_dist = self.view_range + 1 local objs = minetest.get_objects_inside_radius(s, self.view_range) for n = 1, #objs do obj = objs[n]:get_luaentity() if obj and obj.type == "monster" then p = obj.object:get_pos() sp = s local dist = vector.distance(p, s) -- aim higher to make looking up hills more realistic p.y = p.y + 1 sp.y = sp.y + 1 if dist < min_dist and self:line_of_sight( sp, p, 2) == true then min_dist = dist min_player = obj.object end end end if min_player then self:do_attack(min_player) end end -- dogshoot attack switch and counter function function mob_class:dogswitch(dtime) -- switch mode not activated if not self.dogshoot_switch or not dtime then return 0 end self.dogshoot_count = self.dogshoot_count + dtime if (self.dogshoot_switch == 1 and self.dogshoot_count > self.dogshoot_count_max) or (self.dogshoot_switch == 2 and self.dogshoot_count > self.dogshoot_count2_max) then self.dogshoot_count = 0 if self.dogshoot_switch == 1 then self.dogshoot_switch = 2 else self.dogshoot_switch = 1 end end return self.dogshoot_switch end -- no damage to nodes explosion function mob_class:safe_boom(pos, strength) minetest.sound_play(self.sounds and self.sounds.explode or "tnt_explode", { pos = pos, gain = 1.0, max_hear_distance = self.sounds and self.sounds.distance or 32 }, true) local radius = strength entity_physics(pos, radius) mcl_mobs.effect(pos, 32, "mcl_particles_smoke.png", radius * 3, radius * 5, radius, 1, 0) end -- make explosion with protection and tnt mod check function mob_class:boom(pos, strength, fire) if mobs_griefing and not minetest.is_protected(pos, "") then mcl_explosions.explode(pos, strength, { drop_chance = 1.0, fire = fire }, self.object) else mcl_mobs.mob_class.safe_boom(self, pos, strength) --need to call it this way bc self is the "arrow" object here end -- delete the object after it punched the player to avoid nil entities in e.g. mcl_shields!! self.object:remove() end -- deal damage and effects when mob punched function mob_class:on_punch(hitter, tflp, tool_capabilities, dir) -- custom punch function if self.do_punch then -- when false skip going any further if self.do_punch(self, hitter, tflp, tool_capabilities, dir) == false then return end end -- error checking when mod profiling is enabled if not tool_capabilities then minetest.log("warning", "[mobs] Mod profiling enabled, damage not enabled") return end local is_player = hitter:is_player() if is_player then -- is mob protected? if self.protected and minetest.is_protected(self.object:get_pos(), hitter:get_player_name()) then return end if minetest.is_creative_enabled(hitter:get_player_name()) then self.health = 0 end -- set/update 'drop xp' timestamp if hitted by player self.xp_timestamp = minetest.get_us_time() end -- punch interval local weapon = hitter:get_wielded_item() local punch_interval = 1.4 -- exhaust attacker if is_player then mcl_hunger.exhaust(hitter:get_player_name(), mcl_hunger.EXHAUST_ATTACK) end -- calculate mob damage local damage = 0 local armor = self.object:get_armor_groups() or {} local tmp -- quick error check incase it ends up 0 (serialize.h check test) if tflp == 0 then tflp = 0.2 end for group,_ in pairs( (tool_capabilities.damage_groups or {}) ) do tmp = tflp / (tool_capabilities.full_punch_interval or 1.4) if tmp < 0 then tmp = 0.0 elseif tmp > 1 then tmp = 1.0 end damage = damage + (tool_capabilities.damage_groups[group] or 0) * tmp * ((armor[group] or 0) / 100.0) end if weapon then local fire_aspect_level = mcl_enchanting.get_enchantment(weapon, "fire_aspect") if fire_aspect_level > 0 then mcl_burning.set_on_fire(self.object, fire_aspect_level * 4) end end -- check for tool immunity or special damage for n = 1, #self.immune_to do if self.immune_to[n][1] == weapon:get_name() then damage = self.immune_to[n][2] or 0 break end end -- healing if damage <= -1 then self.health = self.health - math.floor(damage) return end if tool_capabilities then punch_interval = tool_capabilities.full_punch_interval or 1.4 end -- add weapon wear manually -- Required because we have custom health handling ("health" property) if minetest.is_creative_enabled("") ~= true and tool_capabilities then if tool_capabilities.punch_attack_uses then -- Without this delay, the wear does not work. Quite hacky ... minetest.after(0, function(name) local player = minetest.get_player_by_name(name) if not player then return end local weapon = hitter:get_wielded_item(player) local def = weapon:get_definition() if def.tool_capabilities and def.tool_capabilities.punch_attack_uses then local wear = math.floor(65535/tool_capabilities.punch_attack_uses) weapon:add_wear(wear) hitter:set_wielded_item(weapon) end end, hitter:get_player_name()) end end local die = false if damage >= 0 then -- only play hit sound and show blood effects if damage is 1 or over; lower to 0.1 to ensure armor works appropriately. if damage >= 0.1 then -- weapon sounds if weapon:get_definition().sounds ~= nil then local s = math.random(0, #weapon:get_definition().sounds) minetest.sound_play(weapon:get_definition().sounds[s], { object = self.object, --hitter, max_hear_distance = 8 }, true) else minetest.sound_play("default_punch", { object = self.object, max_hear_distance = 5 }, true) end self:damage_effect(damage) -- do damage self.health = self.health - damage -- skip future functions if dead, except alerting others if self:check_for_death( "hit", {type = "punch", puncher = hitter}) then die = true end end -- knock back effect (only on full punch) if self.knock_back and tflp >= punch_interval then -- direction error check dir = dir or {x = 0, y = 0, z = 0} local v = self.object:get_velocity() if not v then return end local r = 1.4 - math.min(punch_interval, 1.4) local kb = r * (math.abs(v.x)+math.abs(v.z)) local up = 2 if die==true then kb=kb*2 end -- if already in air then dont go up anymore when hit if math.abs(v.y) > 0.1 or self.fly then up = 0 end -- check if tool already has specific knockback value if tool_capabilities.damage_groups["knockback"] then kb = tool_capabilities.damage_groups["knockback"] else kb = kb * 1.5 end local luaentity if hitter then luaentity = hitter:get_luaentity() end if hitter and is_player then local wielditem = hitter:get_wielded_item() kb = kb + 3 * mcl_enchanting.get_enchantment(wielditem, "knockback") elseif luaentity and luaentity._knockback then kb = kb + luaentity._knockback end self._kb_turn = true self._turn_to=self.object:get_yaw()-1.57 self.frame_speed_multiplier=2.3 if self.animation.run_end then self:set_animation( "run") elseif self.animation.walk_end then self:set_animation( "walk") end minetest.after(0.2, function() if self and self.object then self.frame_speed_multiplier=1 self._kb_turn = false end end) self.object:add_velocity({ x = dir.x * kb, y = up*2, z = dir.z * kb }) self.pause_timer = 0.25 end end -- END if damage -- if skittish then run away if hitter and is_player and hitter:get_pos() and not die and self.runaway == true and self.state ~= "flop" then local yaw = self:set_yaw( minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) minetest.after(0.2,function() if self and self.object and self.object:get_pos() and hitter and is_player and hitter:get_pos() then yaw = self:set_yaw( minetest.dir_to_yaw(vector.direction(hitter:get_pos(), self.object:get_pos()))) self:set_velocity( self.run_velocity) end end) self.state = "runaway" self.runaway_timer = 0 self.following = nil end local name = hitter:get_player_name() or "" -- attack puncher and call other mobs for help if self.passive == false and self.state ~= "flop" and (self.child == false or self.type == "monster") and hitter:get_player_name() ~= self.owner and not mcl_mobs.invis[ name ] then if not die then -- attack whoever punched mob self.state = "" self:do_attack(hitter) self._aggro= true end -- alert others to the attack local objs = minetest.get_objects_inside_radius(hitter:get_pos(), self.view_range) local obj = nil for n = 1, #objs do obj = objs[n]:get_luaentity() if obj then -- only alert members of same mob or friends if obj.group_attack and obj.state ~= "attack" and obj.owner ~= name then if obj.name == self.name then obj:do_attack(hitter) elseif type(obj.group_attack) == "table" then for i=1, #obj.group_attack do if obj.name == obj.group_attack[i] then obj._aggro = true obj:do_attack(hitter) break end end end end -- have owned mobs attack player threat if obj.owner == name and obj.owner_loyal then obj:do_attack(self.object) end end end end end function mob_class:check_aggro(dtime) if not self._aggro or not self.attack then return end if not self._check_aggro_timer or self._check_aggro_timer > 5 then self._check_aggro_timer = 0 if not self.attack:get_pos() or vector.distance(self.attack:get_pos(),self.object:get_pos()) > 128 then self._aggro = nil self.attack = nil self.state = "stand" end end self._check_aggro_timer = self._check_aggro_timer + dtime end