local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs local mob_class = mcl_mobs.mob_class local damage_enabled = minetest.settings:get_bool("enable_damage") local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false -- pathfinding settings local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching local stuck_path_timeout = 10 -- how long will mob follow path before giving up local enable_pathfinding = true local TIME_TO_FORGET_TARGET = 15 local PI = math.pi local HALFPI = PI * 0.5 local random = math.random local min = math.min local floor = math.floor local ceil = math.ceil local abs = math.abs local cos = math.cos local sin = math.sin local atan2 = math.atan2 local vector_offset = vector.offset local vector_new = vector.new local vector_copy = vector.copy local vector_distance = vector.distance -- check if daytime and also if mob is docile during daylight hours function mob_class:day_docile() return self.docile_by_day == true and self.time_of_day > 0.2 and self.time_of_day < 0.8 end -- get this mob to attack the object function mob_class:do_attack(object) if self.state == "attack" or self.state == "die" then return end if object:is_player() and not minetest.settings:get_bool("enable_damage") then return end self.attack = object self.state = "attack" -- TODO: Implement war_cry sound without being annoying --if random(0, 100) < 90 then --self:mob_sound("war_cry", true) --end end -- blast damage to entities nearby local function entity_physics(pos, radius) radius = radius * 2 local objs = minetest.get_objects_inside_radius(pos, radius) local obj_pos, dist for n = 1, #objs do obj_pos = objs[n]:get_pos() dist = vector_distance(pos, obj_pos) if dist < 1 then dist = 1 end local damage = floor((4 / dist) * radius) local ent = objs[n]:get_luaentity() -- punches work on entities AND players objs[n]:punch(objs[n], 1.0, { full_punch_interval = 1.0, damage_groups = {fleshy = damage}, }, pos) end end function mob_class:entity_physics(pos,radius) return entity_physics(pos,radius) end local los_switcher = false local height_switcher = false -- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3 function mob_class:smart_mobs(s, p, dist, dtime) local s1 = self.path.lastpos local target_pos = self.attack:get_pos() -- is it becoming stuck? if abs(s1.x - s.x) + abs(s1.z - s.z) < .5 then self.path.stuck_timer = self.path.stuck_timer + dtime else self.path.stuck_timer = 0 end self.path.lastpos = vector_copy(s) local use_pathfind = false local has_lineofsight = minetest.line_of_sight(vector_offset(s, 0, .5, 0), vector_offset(target_pos, 0, 1.5, 0), .2) -- im stuck, search for path if not has_lineofsight then if los_switcher == true then use_pathfind = true los_switcher = false end -- cannot see target! else if los_switcher == false then los_switcher = true use_pathfind = false minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end -- can see target! end if (self.path.stuck_timer > stuck_timeout and not self.path.following) then use_pathfind = true self.path.stuck_timer = 0 minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then use_pathfind = true self.path.stuck_timer = 0 minetest.after(1, function(self) if not self.object:get_luaentity() then return end if has_lineofsight then self.path.following = false end end, self) end if abs(s.y - target_pos.y) > self.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, s.z = floor(s.x + 0.5), floor(s.z + 0.5) local ssight, sground = minetest.line_of_sight(s, vector_offset(s, 0, -4, 0), 1) -- determine node above ground if not ssight then s.y = sground.y + 1 end local p1 = self.attack:get_pos() p1 = vector_new(floor(p1.x + 0.5), floor(p1.y + 0.5), floor(p1.z + 0.5)) local dropheight = 12 if self.fear_height ~= 0 then dropheight = self.fear_height end local jumpheight = 0 if self.jump and self.jump_height >= 4 then jumpheight = min(ceil(self.jump_height * 0.25), 4) elseif self.stepheight > 0.5 then jumpheight = 1 end self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch") self.state = "" self:do_attack(self.attack) -- no path found, try something else if not self.path.way then self.path.following = false -- lets make way by digging/building if not accessible if self.pathfinding == 2 and mobs_griefing then -- is player higher than mob? if s.y < p1.y then -- build upwards if not minetest.is_protected(s, "") then local ndef1 = minetest.registered_nodes[self.standing_in] if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then minetest.set_node(s, {name = mcl_mobs.fallback_node}) end end local sheight = ceil(self.collisionbox[5]) + 1 -- assume mob is 2 blocks high so it digs above its head s.y = s.y + sheight -- remove one block above to make room to jump if not minetest.is_protected(s, "") then local node1 = node_ok(s, "air").name local ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.set_node(s, {name = "air"}) minetest.add_item(s, ItemStack(node1)) end end s.y = s.y - sheight self.object:set_pos(vector_offset(s, 0, 2, 0)) else -- dig 2 blocks to make door toward player direction local yaw1 = self.object:get_yaw() + HALFPI local p1 = vector_offset(s, cos(yaw1), 0, sin(yaw1)) if not minetest.is_protected(p1, "") then local node1 = node_ok(p1, "air").name local ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.add_item(p1, ItemStack(node1)) minetest.set_node(p1, {name = "air"}) end p1.y = p1.y + 1 node1 = node_ok(p1, "air").name ndef1 = minetest.registered_nodes[node1] if node1 ~= "air" and node1 ~= "ignore" and ndef1 and not ndef1.groups.level and not ndef1.groups.unbreakable and not ndef1.groups.liquid then minetest.add_item(p1, ItemStack(node1)) minetest.set_node(p1, {name = "air"}) end end end end -- will try again in 2 seconds self.path.stuck_timer = stuck_timeout - 2 elseif s.y < p1.y and (not self.fly) then self:do_jump() --add jump to pathfinding self.path.following = true -- Yay, I found path! -- TODO: Implement war_cry sound without being annoying --self:mob_sound("war_cry", true) else self:set_velocity(self.walk_velocity) -- follow path now that it has it self.path.following = true end end end -- specific attacks local specific_attack = function(list, what) -- no list so attack default (player, animals etc.) if list == nil then return true end -- found entity on list to attack? for no = 1, #list do if list[no] == what then return true end end return false end -- find someone to attack function mob_class:monster_attack() if not damage_enabled or self.passive ~= false or self.state == "attack" or self:day_docile() then return end local s = self.object:get_pos() local p, sp, dist local player, obj, min_player local type, name = "", "" local min_dist = self.view_range + 1 local blacklist_attack = {} local objs = minetest.get_objects_inside_radius(s, self.view_range) for n = 1, #objs do if not objs[n]:is_player() then obj = objs[n]:get_luaentity() if obj then player = obj.object name = obj.name or "" end if obj and obj.type == self.type and obj.passive == false and obj.state == "attack" and obj.attack then table.insert(blacklist_attack, obj.attack) end end end for n = 1, #objs do if objs[n]:is_player() then if mcl_mobs.invis[ objs[n]:get_player_name() ] or (not self:object_in_range(objs[n])) then type = "" elseif (self.type == "monster" or self._aggro) then -- self.aggro made player be attacked by npc again if out of range then back in again -- Does it serve a purpose other than that? player = objs[n] type = "player" name = "player" end else obj = objs[n]:get_luaentity() if obj then player = obj.object type = obj.type name = obj.name or "" end end -- find specific mob to attack, failing that attack player/npc/animal if specific_attack(self.specific_attack, name) and (type == "player" or ( type == "npc" and self.attack_npcs ) or (type == "animal" and self.attack_animals == true) or (self.extra_hostile and not self.attack_exception(player))) then p = player:get_pos() sp = s dist = vector_distance(p, s) -- aim higher to make looking up hills more realistic p.y = p.y + 1 sp.y = sp.y + 1 local attacked_p = false for c=1, #blacklist_attack do if blacklist_attack[c] == player then attacked_p = true end end -- choose closest player to attack local line_of_sight = self:line_of_sight( sp, p, 2) == true if dist < min_dist and not attacked_p and line_of_sight then min_dist = dist min_player = player end end end if not min_player and #blacklist_attack > 0 then min_player=blacklist_attack[random(#blacklist_attack)] end -- attack player if min_player then self:do_attack(min_player) end end -- npc, find closest monster to attack function mob_class:npc_attack() if self.type ~= "npc" or not self.attacks_monsters or self.state == "attack" then return end local p, sp, obj, min_player local s = self.object:get_pos() local min_dist = self.view_range + 1 local objs = minetest.get_objects_inside_radius(s, self.view_range) for n = 1, #objs do obj = objs[n]:get_luaentity() if obj and obj.type == "monster" then p = obj.object:get_pos() sp = s local dist = vector_distance(p, s) -- aim higher to make looking up hills more realistic p.y = p.y + 1 sp.y = sp.y + 1 if dist < min_dist and self:line_of_sight( sp, p, 2) == true then min_dist = dist min_player = obj.object end end end if min_player then self:do_attack(min_player) end end -- dogshoot attack switch and counter function function mob_class:dogswitch(dtime) -- switch mode not activated if not self.dogshoot_switch or not dtime then return 0 end self.dogshoot_count = self.dogshoot_count + dtime if (self.dogshoot_switch == 1 and self.dogshoot_count > self.dogshoot_count_max) or (self.dogshoot_switch == 2 and self.dogshoot_count > self.dogshoot_count2_max) then self.dogshoot_count = 0 if self.dogshoot_switch == 1 then self.dogshoot_switch = 2 else self.dogshoot_switch = 1 end end return self.dogshoot_switch end -- no damage to nodes explosion function mob_class:safe_boom(pos, strength) minetest.sound_play(self.sounds and self.sounds.explode or "tnt_explode", { pos = pos, gain = 1.0, max_hear_distance = self.sounds and self.sounds.distance or 32 }, true) local radius = strength entity_physics(pos, radius) mcl_mobs.effect(pos, 32, "mcl_particles_smoke.png", radius * 3, radius * 5, radius, 1, 0) end -- make explosion with protection and tnt mod check function mob_class:boom(pos, strength, fire) if mobs_griefing and not minetest.is_protected(pos, "") then mcl_explosions.explode(pos, strength, { drop_chance = 1.0, fire = fire }, self.object) else mcl_mobs.mob_class.safe_boom(self, pos, strength) --need to call it this way bc self is the "arrow" object here end -- delete the object after it punched the player to avoid nil entities in e.g. mcl_shields!! self.object:remove() end -- deal damage and effects when mob punched function mob_class:on_punch(hitter, tflp, tool_capabilities, dir) local is_player = hitter:is_player() local mob_pos = self.object:get_pos() local player_pos = hitter:get_pos() if is_player then -- is mob out of reach? if vector_distance(mob_pos, player_pos) > 3 then return end -- is mob protected? if self.protected and minetest.is_protected(mob_pos, hitter:get_player_name()) then return end mcl_potions.update_haste_and_fatigue(hitter) end local time_now = minetest.get_us_time() local time_diff = time_now - self.invul_timestamp -- check for invulnerability time in microseconds (0.5 second) if time_diff <= 500000 and time_diff >= 0 then return end -- custom punch function if self.do_punch then -- when false skip going any further if self.do_punch(self, hitter, tflp, tool_capabilities, dir) == false then return end end -- error checking when mod profiling is enabled if not tool_capabilities then minetest.log("warning", "[mobs] Mod profiling enabled, damage not enabled") return end local time_now = minetest.get_us_time() if is_player then if minetest.is_creative_enabled(hitter:get_player_name()) then self.health = 0 end -- set/update 'drop xp' timestamp if hitted by player self.xp_timestamp = time_now end -- punch interval local 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) tmp = tmp < 0 and 0 or (tmp > 1 and 1 or tmp) damage = damage + (tool_capabilities.damage_groups[group] or 0) * tmp * ((armor[group] or 0) / 100.0) end -- strength and weakness effects local strength = mcl_potions.get_effect(hitter, "strength") local weakness = mcl_potions.get_effect(hitter, "weakness") local str_fac = strength and strength.factor or 1 local weak_fac = weakness and weakness.factor or 1 damage = damage * str_fac * weak_fac if weapon then local fire_aspect_level = mcl_enchanting.get_enchantment(weapon, "fire_aspect") if fire_aspect_level > 0 then mcl_burning.set_on_fire(self.object, fire_aspect_level * 4) end end -- check for tool immunity or special damage for n = 1, #self.immune_to do if self.immune_to[n][1] == weapon:get_name() then damage = self.immune_to[n][2] or 0 break end end -- healing if damage <= -1 then self.health = self.health - floor(damage) return end if tool_capabilities then punch_interval = tool_capabilities.full_punch_interval or 1.4 end -- add weapon wear manually -- Required because we have custom health handling ("health" property) if minetest.is_creative_enabled("") ~= true and tool_capabilities then if tool_capabilities.punch_attack_uses then -- Without this delay, the wear does not work. Quite hacky ... minetest.after(0, function(name) local player = minetest.get_player_by_name(name) if not player then return end local weapon = hitter:get_wielded_item(player) local def = weapon:get_definition() if def.tool_capabilities and def.tool_capabilities.punch_attack_uses then local wear = floor(65535/tool_capabilities.punch_attack_uses) weapon:add_wear(wear) tt.reload_itemstack_description(weapon) -- update tooltip hitter:set_wielded_item(weapon) end end, hitter:get_player_name()) end end local die = false if damage >= 0 then -- only play hit sound and show blood effects if damage is 1 or over; lower to 0.1 to ensure armor works appropriately. if damage >= 0.1 then -- weapon sounds if weapon:get_definition().sounds ~= nil then local s = random(0, #weapon:get_definition().sounds) minetest.sound_play(weapon:get_definition().sounds[s], { object = self.object, --hitter, max_hear_distance = 8 }, true) else minetest.sound_play("default_punch", { object = self.object, max_hear_distance = 5 }, true) end self:damage_effect(damage) -- do damage self.health = self.health - damage -- give invulnerability self.invul_timestamp = time_now -- skip future functions if dead, except alerting others if self:check_for_death( "hit", {type = "punch", puncher = hitter}) then die = true end end -- knock back effect (only on full punch) if self.knock_back and tflp >= punch_interval then -- direction error check dir = dir or vector_zero() local v = self.object:get_velocity() if not v then return end local r = 1.4 - min(punch_interval, 1.4) local kb = r * (abs(v.x)+abs(v.z)) local up = 2.625 if die then kb = kb * 1.25 end -- if already in air then dont go up anymore when hit if 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.25 end local luaentity = hitter and hitter:get_luaentity() if hitter and is_player then local wielditem = hitter:get_wielded_item() kb = kb + 9 * mcl_enchanting.get_enchantment(wielditem, "knockback") -- add player velocity to mob knockback local hv = hitter:get_velocity() local dir_dot = (hv.x * dir.x) + (hv.z * dir.z) local player_mag = ((hv.x * hv.x) + (hv.z * hv.z))^0.5 local mob_mag = ((v.x * v.x) + (v.z * v.z))^0.5 if dir_dot > 0 and mob_mag <= player_mag * 0.625 then kb = kb + (abs(hv.x) + abs(hv.z)) * r end elseif luaentity and luaentity._knockback and die == false then kb = kb + luaentity._knockback elseif luaentity and luaentity._knockback and die == true then kb = kb + luaentity._knockback * 0.25 end self._kb_turn = true self:turn_by(HALFPI, .1) -- knockback turn self.frame_speed_multiplier=2.3 if self.animation.run_end then self:set_animation("run") elseif self.animation.walk_end then self:set_animation("walk") end minetest.after(0.2, function() if self and self.object then self.frame_speed_multiplier=1 self._kb_turn = false end end) self.object:add_velocity(vector_new(dir.x * kb, up*2, dir.z * kb )) self.pause_timer = 0.25 end end -- END if damage -- if skittish then run away if hitter and is_player and hitter:get_pos() and not die and self.runaway == true and self.state ~= "flop" then local hp, sp = hitter:get_pos(), self.object:get_pos() self:turn_in_direction(sp.x - hp.x, sp.z - hp.z, 1) minetest.after(0.2,function() if self and self.object and hitter and is_player then local hp, sp = hitter:get_pos(), self.object:get_pos() if hp and sp then self:turn_in_direction(sp.x - hp.x, sp.z - hp.z, 1) self:set_velocity(self.run_velocity) end end end) self.state = "runaway" self.runaway_timer = 0 self.following = nil end local name = hitter:get_player_name() or "" -- attack puncher if self.passive == false and self.state ~= "flop" and (self.child == false or self.type == "monster") and hitter:get_player_name() ~= self.owner and not mcl_mobs.invis[ name ] then if not die then -- attack whoever punched mob self.state = "" self:do_attack(hitter) self._aggro= true end end -- alert others to the attack local alert_pos = hitter:get_pos() if alert_pos then local objs = minetest.get_objects_inside_radius(alert_pos, self.view_range) local obj = nil for n = 1, #objs do obj = objs[n]:get_luaentity() if obj then -- only alert members of same mob or friends if obj.group_attack and obj.state ~= "attack" and obj.owner ~= name then if obj.name == self.name then obj:do_attack(hitter) elseif type(obj.group_attack) == "table" then for i=1, #obj.group_attack do if obj.group_attack[i] == self.name then obj._aggro = true obj:do_attack(hitter) break end end end end -- have owned mobs attack player threat if obj.owner == name and obj.owner_loyal then obj:do_attack(self.object) end end end end end function mob_class:check_aggro(dtime) if not self._aggro or not self.attack then return end if not self._check_aggro_timer then self._check_aggro_timer = 0 end if self._check_aggro_timer > 5 then self._check_aggro_timer = 0 if self.attack then -- TODO consider removing this in favour of what is done in do_states_attack -- Attack is dropped in do_states_attack if out of range, so won't even trigger here -- I do not think this code does anything. Are mobs still loaded in at 128? if not self.attack:get_pos() or vector_distance(self.attack:get_pos(),self.object:get_pos()) > 128 then self._aggro = nil self.attack = nil self.state = "stand" end end end self._check_aggro_timer = self._check_aggro_timer + dtime end local function clear_aggro(self) self.state = "stand" self:set_velocity( 0) self:set_animation( "stand") self.attack = nil self._aggro = nil self.v_start = false self.timer = 0 self.blinktimer = 0 self.path.way = nil end function mob_class:do_states_attack(dtime) self.timer = self.timer + dtime if self.timer > 100 then self.timer = 1 end local s = self.object:get_pos() if not s then return end local p = self.attack:get_pos() or s local yaw = self.object:get_yaw() or 0 -- stop attacking if player invisible or out of range if not self.attack or not self.attack:get_pos() or not self:object_in_range(self.attack) or self.attack:get_hp() <= 0 or (self.attack:is_player() and mcl_mobs.invis[ self.attack:get_player_name() ]) then clear_aggro(self) return end local target_line_of_sight = self:target_visible(s) if not target_line_of_sight then if self.target_time_lost then local time_since_seen = os.time() - self.target_time_lost if time_since_seen > TIME_TO_FORGET_TARGET then self.target_time_lost = nil clear_aggro(self) return end else self.target_time_lost = os.time() end else self.target_time_lost = nil end -- calculate distance from mob and enemy local dist = vector_distance(p, s) if self.attack_type == "explode" then if target_line_of_sight then self:turn_in_direction(p.x - s.x, p.z - s.z, 1) end local node_break_radius = self.explosion_radius or 1 local entity_damage_radius = self.explosion_damage_radius or (node_break_radius * 2) -- start timer when in reach and line of sight if not self.v_start and dist <= self.reach and target_line_of_sight then self.v_start = true self.timer = 0 self.blinktimer = 0 self:mob_sound("fuse", nil, false) -- stop timer if out of reach or direct line of sight elseif self.allow_fuse_reset and self.v_start and (dist >= self.explosiontimer_reset_radius or not target_line_of_sight) then self.v_start = false self.timer = 0 self.blinktimer = 0 self.blinkstatus = false self:remove_texture_mod("^[brighten") end -- walk right up to player unless the timer is active if self.v_start and (self.stop_to_explode or dist < self.reach) or not target_line_of_sight then self:set_velocity(0) else self:set_velocity(self.run_velocity) end if self.animation and self.animation.run_start then self:set_animation("run") else self:set_animation("walk") end if self.v_start then self.timer = self.timer + dtime self.blinktimer = (self.blinktimer or 0) + dtime self:set_animation("fuse") if self.blinktimer > 0.2 then self.blinktimer = 0 if self.blinkstatus then self:remove_texture_mod("^[brighten") else self:add_texture_mod("^[brighten") end self.blinkstatus = not self.blinkstatus end if self.timer > self.explosion_timer then local pos = self.object:get_pos() if mobs_griefing and not minetest.is_protected(pos, "") then mcl_explosions.explode(mcl_util.get_object_center(self.object), self.explosion_strength, { drop_chance = 1.0 }, self.object) else minetest.sound_play(self.sounds.explode, { pos = pos, gain = 1.0, max_hear_distance = self.sounds.distance or 32 }, true) self:entity_physics(pos,entity_damage_radius) mcl_mobs.effect(pos, 32, "mcl_particles_smoke.png", nil, nil, node_break_radius, 1, 0) end mcl_burning.extinguish(self.object) self.object:remove() return true end end elseif self.attack_type == "dogfight" or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 2) and (dist >= self.avoid_distance or not self.shooter_avoid_enemy) or (self.attack_type == "dogshoot" and dist <= self.reach and self:dogswitch() == 0) then if self.fly and dist > self.reach then local p1, p2 = s, p local me_y, p_y = floor(p1.y), floor(p2.y + 1) local v = self.object:get_velocity() if self:flight_check( s) then if me_y < p_y then self.object:set_velocity(vector_new(v.x, 1 * self.walk_velocity, v.z)) elseif me_y > p_y then self.object:set_velocity(vector_new(v.x, -1 * self.walk_velocity, v.z)) end else if me_y < p_y then self.object:set_velocity(vector_new(v.x, 0.01, v.z)) elseif me_y > p_y then self.object:set_velocity(vector_new(v.x, -0.01, v.z)) end end end -- rnd: new movement direction if self.path.following and self.path.way and self.attack_type ~= "dogshoot" then -- no paths longer than 50 if #self.path.way > 50 or dist < self.reach then self.path.following = false return end local p1 = self.path.way[1] if not p1 then self.path.following = false return end if abs(p1.x - s.x) + abs(p1.z - s.z) < 0.6 then -- reached waypoint, remove it from queue table.remove(self.path.way, 1) end -- set new temporary target p = vector_copy(p1) end self:turn_in_direction(p.x - s.x, p.z - s.z, 10) -- move towards enemy if beyond mob reach if dist > self.reach then -- path finding by rnd if enable_pathfinding and self.pathfinding then self:smart_mobs(s, p, dist, dtime) end if self:is_at_cliff_or_danger() then self:set_velocity(0) self:set_animation("stand") --self:turn_by(PI * (random() - 0.5), 10) else if self.path.stuck then self:set_velocity(self.walk_velocity) else self:set_velocity(self.run_velocity) end if self.animation and self.animation.run_start then self:set_animation("run") else self:set_animation("walk") end end else -- rnd: if inside reach range self.path.stuck = false self.path.stuck_timer = 0 self.path.following = false -- not stuck anymore self:set_velocity( 0) local attack_frequency = self.attack_frequency or 1 if self.timer > attack_frequency then self.timer = 0 if not self.custom_attack then if self.double_melee_attack and random(1, 2) == 1 then self:set_animation("punch2") else self:set_animation("punch") end if self:line_of_sight(vector_offset(p, 0, .5, 0), vector_offset(s, 0, .5, 0)) == true then self:mob_sound("attack") -- punch player (or what player is attached to) local attached = self.attack:get_attach() if attached then self.attack = attached end self.attack:punch(self.object, 1.0, { full_punch_interval = 1.0, damage_groups = {fleshy = self.damage} }, nil) if self.dealt_effect then mcl_potions.give_effect_by_level(self.dealt_effect.name, self.attack, self.dealt_effect.level, self.dealt_effect.dur) end end else self.custom_attack(self, p) end end end elseif self.attack_type == "shoot" or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 1) or (self.attack_type == "dogshoot" and (dist > self.reach or dist < self.avoid_distance and self.shooter_avoid_enemy) and self:dogswitch() == 0) then local vec = vector_new(p.x - s.x, p.y - s.y - 1, p.z - s.z) local dist = (vec.x*vec.x + vec.y*vec.y + vec.z*vec.z)^0.5 self:turn_in_direction(vec.x, vec.z, 10) if self.strafes then if not self.strafe_direction then self.strafe_direction = HALFPI end if random(40) == 1 then self.strafe_direction = self.strafe_direction * -1 end local dir = -atan2(p.x - s.x, p.z - s.z) self.acc = vector_new(-sin(dir + self.strafe_direction) * 0.8, 0, cos(dir + self.strafe_direction) * 0.8) --stay away from player so as to shoot them if self.avoid_distance and dist < self.avoid_distance and self.shooter_avoid_enemy then local f = 0.3 * (self.avoid_distance - dist) / self.avoid_distance self.acc.x, self.acc.z = self.acc.x - sin(dir) * f, self.acc.z + cos(dir) * f end else self:set_velocity(0) self:set_animation("stand") end local p = self.object:get_pos() p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) * 0.5 if self.shoot_interval and self.timer > self.shoot_interval and random(1, 100) <= 60 and not minetest.raycast(vector_offset(p, 0, self.shoot_offset, 0), vector_offset(self.attack:get_pos(), 0, 1.5, 0), false, false):next() then self.timer = 0 self:set_animation( "shoot") -- play shoot attack sound self:mob_sound("shoot_attack") -- Shoot arrow if minetest.registered_entities[self.arrow] then local arrow, ent local v = 1 if not self.shoot_arrow then self.firing = true minetest.after(1, function() self.firing = false end) arrow = minetest.add_entity(p, self.arrow) ent = arrow:get_luaentity() v = ent.velocity or v ent.switch = 1 ent.owner_id = tostring(self.object) -- add unique owner id to arrow -- important for mcl_shields ent._shooter = self.object ent._saved_shooter_pos = self.object:get_pos() if ent.homing then ent._target = self.attack end end -- offset makes shoot aim accurate vec.y = vec.y + self.shoot_offset vec.x, vec.y, vec.z = vec.x * (v / dist), vec.y * (v / dist), vec.z * (v / dist) if self.shoot_arrow then vec = vector.normalize(vec) self:shoot_arrow(p, vec) else arrow:set_velocity(vec) end end end elseif self.attack_type == "custom" and self.attack_state then self.attack_state(self, dtime) end if self.on_attack then self.on_attack(self, dtime) end end