-- Mobs Api (2nd February 2016) mobs = {} mobs.mod = "redo" -- Load settings local damage_enabled = minetest.setting_getbool("enable_damage") local peaceful_only = minetest.setting_getbool("only_peaceful_mobs") local disable_blood = minetest.setting_getbool("mobs_disable_blood") local creative = minetest.setting_getbool("creative_mode") local spawn_protected = tonumber(minetest.setting_get("mobs_spawn_protected")) or 1 local remove_far = minetest.setting_getbool("remove_far_mobs") -- PATHFINDING settings local enable_pathfinding = true local enable_pathfind_digging = true local stuck_timeout = 5 -- how long before mob gets stuck in place and starts searching local stuck_path_timeout = 15 -- how long will mob follow path before giving up -- internal functions local pi = math.pi local square = math.sqrt do_attack = function(self, player) if self.state ~= "attack" then if math.random(0,100) < 90 and self.sounds.war_cry then minetest.sound_play(self.sounds.war_cry,{ object = self.object, max_hear_distance = self.sounds.distance }) end self.state = "attack" self.attack = player end end set_velocity = function(self, v) local yaw = (self.object:getyaw() + self.rotate) or 0 self.object:setvelocity({ x = math.sin(yaw) * -v, y = self.object:getvelocity().y, z = math.cos(yaw) * v }) end get_velocity = function(self) local v = self.object:getvelocity() return (v.x * v.x + v.z * v.z) ^ 0.5 end set_animation = function(self, type) if not self.animation then return end self.animation.current = self.animation.current or "" if type == "stand" and self.animation.current ~= "stand" then if self.animation.stand_start and self.animation.stand_end and self.animation.speed_normal then self.object:set_animation({ x = self.animation.stand_start, y = self.animation.stand_end}, self.animation.speed_normal, 0) self.animation.current = "stand" end elseif type == "walk" and self.animation.current ~= "walk" then if self.animation.walk_start and self.animation.walk_end and self.animation.speed_normal then self.object:set_animation({ x = self.animation.walk_start, y = self.animation.walk_end}, self.animation.speed_normal, 0) self.animation.current = "walk" end elseif type == "run" and self.animation.current ~= "run" then if self.animation.run_start and self.animation.run_end and self.animation.speed_run then self.object:set_animation({ x = self.animation.run_start, y = self.animation.run_end}, self.animation.speed_run, 0) self.animation.current = "run" end elseif type == "punch" and self.animation.current ~= "punch" then if self.animation.punch_start and self.animation.punch_end and self.animation.speed_normal then self.object:set_animation({ x = self.animation.punch_start, y = self.animation.punch_end}, self.animation.speed_normal, 0) self.animation.current = "punch" end end end -- particle effects function effect(pos, amount, texture, max_size) minetest.add_particlespawner({ amount = amount, time = 0.25, minpos = pos, maxpos = pos, minvel = {x = -0, y = -2, z = -0}, maxvel = {x = 2, y = 2, z = 2}, minacc = {x = -4, y = -4, z = -4}, maxacc = {x = 4, y = 4, z = 4}, minexptime = 0.1, maxexptime = 1, minsize = 0.5, maxsize = (max_size or 1), texture = texture, }) end -- update nametag colour function update_tag(self) local col = "#00FF00" local qua = self.hp_max / 4 if self.health <= math.floor(qua * 3) then col = "#FFFF00" end if self.health <= math.floor(qua * 2) then col = "#FF6600" end if self.health <= math.floor(qua) then col = "#FF0000" end self.object:set_properties({ nametag = self.nametag, nametag_color = col }) end -- check if mob is dead or only hurt function check_for_death(self) -- return if no change local hp = self.object:get_hp() if hp == self.health then return false end -- still got some health? play hurt sound if hp > 0 then self.health = hp if self.sounds.damage then minetest.sound_play(self.sounds.damage,{ object = self.object, gain = 1.0, max_hear_distance = self.sounds.distance }) end update_tag(self) return false end -- drop items when dead local obj local pos = self.object:getpos() for _,drop in pairs(self.drops) do if math.random(1, drop.chance) == 1 then obj = minetest.add_item(pos, ItemStack(drop.name .. " " .. math.random(drop.min, drop.max))) if obj then obj:setvelocity({ x = math.random(-1, 1), y = 6, z = math.random(-1, 1) }) end end end -- play death sound if self.sounds.death then minetest.sound_play(self.sounds.death,{ object = self.object, gain = 1.0, max_hear_distance = self.sounds.distance }) end -- execute custom death function if self.on_die then self.on_die(self, pos) end self.object:remove() return true end -- check if within map limits (-30911 to 30927) function within_limits(pos, radius) if (pos.x - radius) > -30913 and (pos.x + radius) < 30928 and (pos.y - radius) > -30913 and (pos.y + radius) < 30928 and (pos.z - radius) > -30913 and (pos.z + radius) < 30928 then return true -- within limits end return false -- beyond limits end -- is mob facing a cliff local function is_at_cliff(self) if self.fear_height == 0 then -- if 0, no falling protection! return false end local yaw = self.object:getyaw() local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5) local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5) local pos = self.object:getpos() local ypos = pos.y + self.collisionbox[2] -- just above floor if minetest.line_of_sight( {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, {x = pos.x + dir_x, y = ypos - self.fear_height, z = pos.z + dir_z} , 1) then return true end return false end -- environmental damage (water, lava, fire, light) do_env_damage = function(self) -- feed/tame text timer (so mob 'full' messages dont spam chat) if self.htimer > 0 then self.htimer = self.htimer - 1 end local pos = self.object:getpos() self.time_of_day = minetest.get_timeofday() -- remove mob if beyond map limits if not within_limits(pos, 0) then self.object:remove() return end -- daylight above ground if self.light_damage ~= 0 and pos.y > 0 and self.time_of_day > 0.2 and self.time_of_day < 0.8 and (minetest.get_node_light(pos) or 0) > 12 then self.object:set_hp(self.object:get_hp() - self.light_damage) effect(pos, 5, "tnt_smoke.png") end if self.water_damage ~= 0 or self.lava_damage ~= 0 then pos.y = pos.y + self.collisionbox[2] + 0.1 -- foot level local nod = node_ok(pos, "air") ; --print ("standing in "..nod.name) local nodef = minetest.registered_nodes[nod.name] pos.y = pos.y + 1 -- water if self.water_damage ~= 0 and nodef.groups.water then self.object:set_hp(self.object:get_hp() - self.water_damage) effect(pos, 5, "bubble.png") end -- lava or fire if self.lava_damage ~= 0 and (nodef.groups.lava or nod.name == "fire:basic_flame" or nod.name == "fire:permanent_flame") then self.object:set_hp(self.object:get_hp() - self.lava_damage) effect(pos, 5, "fire_basic_flame.png") end end check_for_death(self) end -- jump if facing a solid node (not fences) do_jump = function(self) if self.fly or self.child then return end local pos = self.object:getpos() -- what is mob standing on? pos.y = pos.y + self.collisionbox[2] - 0.2 local nod = node_ok(pos) --print ("standing on:", nod.name, pos.y) if minetest.registered_nodes[nod.name].walkable == false then return end -- where is front local yaw = self.object:getyaw() local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5) local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5) -- what is in front of mob? local nod = node_ok({ x = pos.x + dir_x, y = pos.y + 0.5, z = pos.z + dir_z }) -- thin blocks that do not need to be jumped if nod.name == "default:snow" then return end --print ("in front:", nod.name, pos.y + 0.5) if minetest.registered_items[nod.name].walkable and not nod.name:find("fence") or self.walk_chance == 0 then local v = self.object:getvelocity() v.y = self.jump_height + 1 v.x = v.x * 2.2 v.z = v.z * 2.2 self.object:setvelocity(v) if self.sounds.jump then minetest.sound_play(self.sounds.jump, { object = self.object, gain = 1.0, max_hear_distance = self.sounds.distance }) end else if self.state ~= "attack" then self.state = "stand" set_animation(self, "stand") end end end -- this is a faster way to calculate distance local get_distance = function(a, b) local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z return square(x * x + y * y + z * z) end -- blast damage to entities nearby (modified from TNT mod) function entity_physics(pos, radius) radius = radius * 2 local objs = minetest.get_objects_inside_radius(pos, radius) local obj_pos, dist for _, obj in pairs(objs) do obj_pos = obj:getpos() dist = math.max(1, get_distance(pos, obj_pos)) local damage = math.floor((4 / dist) * radius) obj:set_hp(obj:get_hp() - damage) end end -- get node but use fallback for nil or unknown function node_ok(pos, fallback) fallback = fallback or "default:dirt" local node = minetest.get_node_or_nil(pos) if not node then return minetest.registered_nodes[fallback] end if minetest.registered_nodes[node.name] then return node end return minetest.registered_nodes[fallback] end -- should mob follow what I'm holding ? function follow_holding(self, clicker) local item = clicker:get_wielded_item() local t = type(self.follow) -- single item if t == "string" and item:get_name() == self.follow then return true -- multiple items elseif t == "table" then for no = 1, #self.follow do if self.follow[no] == item:get_name() then return true end end end return false end local function breed(self) -- child take 240 seconds before growing into adult if self.child == true then self.hornytimer = self.hornytimer + 1 if self.hornytimer > 240 then self.child = false self.hornytimer = 0 self.object:set_properties({ textures = self.base_texture, mesh = self.base_mesh, visual_size = self.base_size, collisionbox = self.base_colbox, }) -- jump when fully grown so not to fall into ground self.object:setvelocity({ x = 0, y = self.jump_height, z = 0 }) end return end -- horny animal can mate for 40 seconds, -- afterwards horny animal cannot mate again for 200 seconds if self.horny == true and self.hornytimer < 240 then self.hornytimer = self.hornytimer + 1 if self.hornytimer >= 240 then self.hornytimer = 0 self.horny = false end end -- find another same animal who is also horny and mate if close enough if self.horny == true and self.hornytimer <= 40 then local pos = self.object:getpos() effect({x = pos.x, y = pos.y + 1, z = pos.z}, 4, "heart.png") local ents = minetest.get_objects_inside_radius(pos, 3) local num = 0 local ent = nil for i, obj in pairs(ents) do ent = obj:get_luaentity() -- check for same animal with different colour local canmate = false if ent then if ent.name == self.name then canmate = true else local entname = string.split(ent.name,":") local selfname = string.split(self.name,":") if entname[1] == selfname[1] then entname = string.split(entname[2],"_") selfname = string.split(selfname[2],"_") if entname[1] == selfname[1] then canmate = true end end end end if ent and canmate == true and ent.horny == true and ent.hornytimer <= 40 then num = num + 1 end -- found your mate? then have a baby if num > 1 then self.hornytimer = 41 ent.hornytimer = 41 -- spawn baby minetest.after(5, function(dtime) local mob = minetest.add_entity(pos, self.name) local ent2 = mob:get_luaentity() local textures = self.base_texture if self.child_texture then textures = self.child_texture[1] end mob:set_properties({ textures = textures, visual_size = { x = self.base_size.x / 2, y = self.base_size.y / 2 }, collisionbox = { self.base_colbox[1] / 2, self.base_colbox[2] / 2, self.base_colbox[3] / 2, self.base_colbox[4] / 2, self.base_colbox[5] / 2, self.base_colbox[6] / 2 }, }) ent2.child = true ent2.tamed = true ent2.owner = self.owner end) num = 0 break end end end end function replace(self, pos) if self.replace_rate and self.child == false and math.random(1, self.replace_rate) == 1 then local pos = self.object:getpos() pos.y = pos.y + self.replace_offset -- print ("replace node = ".. minetest.get_node(pos).name, pos.y) if self.replace_what and self.object:getvelocity().y == 0 and #minetest.find_nodes_in_area(pos, pos, self.replace_what) > 0 then minetest.set_node(pos, {name = self.replace_with}) -- when cow/sheep eats grass, replace wool and milk if self.gotten == true then self.gotten = false self.object:set_properties(self) end end end end -- check if daytime and also if mob is docile during daylight hours function day_docile(self) 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 -- register mob function function mobs:register_mob(name, def) minetest.register_entity(name, { stepheight = def.stepheight or 0.6, name = name, type = def.type, attack_type = def.attack_type, fly = def.fly, fly_in = def.fly_in or "air", owner = def.owner or "", order = def.order or "", on_die = def.on_die, do_custom = def.do_custom, jump_height = def.jump_height or 6, jump_chance = def.jump_chance or 0, drawtype = def.drawtype, -- DEPRECATED, use rotate instead rotate = math.rad(def.rotate or 0), -- 0=front, 90=side, 180=back, 270=side2 lifetimer = def.lifetimer or 180, -- 3 minutes hp_min = def.hp_min or 5, hp_max = def.hp_max or 10, physical = true, collisionbox = def.collisionbox, visual = def.visual, visual_size = def.visual_size or {x = 1, y = 1}, mesh = def.mesh, makes_footstep_sound = def.makes_footstep_sound or false, view_range = def.view_range or 5, walk_velocity = def.walk_velocity or 1, run_velocity = def.run_velocity or 2, damage = def.damage or 0, light_damage = def.light_damage or 0, water_damage = def.water_damage or 0, lava_damage = def.lava_damage or 0, fall_damage = def.fall_damage or 1, fall_speed = def.fall_speed or -10, -- must be lower than -2 (default: -10) drops = def.drops or {}, armor = def.armor, on_rightclick = def.on_rightclick, arrow = def.arrow, shoot_interval = def.shoot_interval, sounds = def.sounds or {}, animation = def.animation, follow = def.follow, jump = def.jump or true, walk_chance = def.walk_chance or 50, attacks_monsters = def.attacks_monsters or false, group_attack = def.group_attack or false, --fov = def.fov or 120, passive = def.passive or false, recovery_time = def.recovery_time or 0.5, knock_back = def.knock_back or 3, blood_amount = def.blood_amount or 5, blood_texture = def.blood_texture or "mobs_blood.png", shoot_offset = def.shoot_offset or 0, floats = def.floats or 1, -- floats in water by default replace_rate = def.replace_rate, replace_what = def.replace_what, replace_with = def.replace_with, replace_offset = def.replace_offset or 0, timer = 0, env_damage_timer = 0, -- only used when state = "attack" tamed = false, pause_timer = 0, horny = false, hornytimer = 0, child = false, gotten = false, health = 0, reach = def.reach or 3, htimer = 0, child_texture = def.child_texture, docile_by_day = def.docile_by_day or false, time_of_day = 0.5, fear_height = def.fear_height or 0, runaway = def.runaway, runaway_timer = 0, on_step = function(self, dtime) local pos = self.object:getpos() local yaw = self.object:getyaw() or 0 -- when lifetimer expires remove mob (except npc and tamed) if self.type ~= "npc" and not self.tamed and self.state ~= "attack" then self.lifetimer = self.lifetimer - dtime if self.lifetimer <= 0 then -- only despawn away from player local objs = minetest.get_objects_inside_radius(pos, 10) for _,oir in pairs(objs) do if oir:is_player() then self.lifetimer = 20 return end end minetest.log("action", "lifetimer expired, removed " .. self.name) effect(pos, 15, "tnt_smoke.png") self.object:remove() return end end if not self.fly then -- floating in water (or falling) local v = self.object:getvelocity() -- going up then apply gravity if v.y > 0.1 then self.object:setacceleration({ x = 0, y = self.fall_speed, z = 0 }) end -- in water then float up if minetest.registered_nodes[node_ok(pos).name].groups.water then if self.floats == 1 then self.object:setacceleration({ x = 0, y = -self.fall_speed / (math.max(1, v.y) ^ 2), z = 0 }) end else -- fall downwards self.object:setacceleration({ x = 0, y = self.fall_speed, z = 0 }) -- fall damage if self.fall_damage == 1 and self.object:getvelocity().y == 0 then local d = self.old_y - self.object:getpos().y if d > 5 then self.object:set_hp(self.object:get_hp() - math.floor(d - 5)) effect(pos, 5, "tnt_smoke.png") if check_for_death(self) then return end end self.old_y = self.object:getpos().y end end end -- knockback timer if self.pause_timer > 0 then self.pause_timer = self.pause_timer - dtime if self.pause_timer < 1 then self.pause_timer = 0 end return end -- attack timer self.timer = self.timer + dtime if self.state ~= "attack" then if self.timer < 1 then return end self.timer = 0 end -- never go over 100 if self.timer > 100 then self.timer = 1 end -- node replace check (cow eats grass etc.) replace(self, pos) -- mob plays random sound at times if self.sounds.random and math.random(1, 100) == 1 then minetest.sound_play(self.sounds.random, { object = self.object, max_hear_distance = self.sounds.distance }) end -- environmental damage timer (every 1 second) self.env_damage_timer = self.env_damage_timer + dtime if (self.state == "attack" and self.env_damage_timer > 1) or self.state ~= "attack" then self.env_damage_timer = 0 do_env_damage(self) -- custom function (defined in mob lua file) if self.do_custom then self.do_custom(self) end end -- find someone to attack if self.type == "monster" and damage_enabled and self.state ~= "attack" and not day_docile(self) then local s = self.object:getpos() local p, sp, dist local player = nil local type = nil local obj = nil local min_dist = self.view_range + 1 local min_player = nil for _,oir in pairs(minetest.get_objects_inside_radius(s, self.view_range)) do if oir:is_player() then player = oir type = "player" else obj = oir:get_luaentity() if obj then player = obj.object type = obj.type end end if type == "player" or type == "npc" then s = self.object:getpos() p = player:getpos() sp = s -- aim higher to make looking up hills more realistic p.y = p.y + 1 sp.y = sp.y + 1 dist = get_distance(p, s) if dist < self.view_range then -- field of view check goes here -- choose closest player to attack if minetest.line_of_sight(sp, p, 2) == true and dist < min_dist then min_dist = dist min_player = player end end end end -- attack player if min_player then do_attack(self, min_player) end end -- npc, find closest monster to attack local min_dist = self.view_range + 1 local min_player = nil if self.type == "npc" and self.attacks_monsters and self.state ~= "attack" then local s = self.object:getpos() local obj = nil for _, oir in pairs(minetest.get_objects_inside_radius(s, self.view_range)) do obj = oir:get_luaentity() if obj and obj.type == "monster" then -- attack monster p = obj.object:getpos() dist = get_distance(p, s) if dist < min_dist then min_dist = dist min_player = obj.object end end end if min_player then do_attack(self, min_player) end end -- breed and grow children breed(self) -- find player to follow if (self.follow ~= "" or self.order == "follow") and not self.following and self.state ~= "attack" and self.state ~= "runaway" then local s, p, dist for _,player in pairs(minetest.get_connected_players()) do s = self.object:getpos() p = player:getpos() dist = get_distance(p, s) if dist < self.view_range then self.following = player break end end end if self.type == "npc" and self.order == "follow" and self.state ~= "attack" and self.owner ~= "" then -- npc stop following player if not owner if self.following and self.owner and self.owner ~= self.following:get_player_name() then self.following = nil end else -- stop following player if not holding specific item if self.following and self.following:is_player() and follow_holding(self, self.following) == false then self.following = nil end end -- follow that thing if self.following then local s = self.object:getpos() local p if self.following:is_player() then p = self.following:getpos() elseif self.following.object then p = self.following.object:getpos() end if p then local dist = get_distance(p, s) -- dont follow if out of range if dist > self.view_range then self.following = nil else local vec = { x = p.x - s.x, y = p.y - s.y, z = p.z - s.z } if vec.x ~= 0 and vec.z ~= 0 then yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate if p.x > s.x then yaw = yaw + pi end self.object:setyaw(yaw) end -- anyone but standing npc's can move along if dist > self.reach and self.order ~= "stand" then if (self.jump and get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0) or (self.object:getvelocity().y == 0 and self.jump_chance > 0) then do_jump(self) end set_velocity(self, self.walk_velocity) if self.walk_chance ~= 0 then set_animation(self, "walk") end else set_velocity(self, 0) set_animation(self, "stand") end return end end end if self.state == "stand" then if math.random(1, 4) == 1 then local lp = nil local s = self.object:getpos() if self.type == "npc" then local o = minetest.get_objects_inside_radius(self.object:getpos(), 3) for _,o in pairs(o) do if o:is_player() then lp = o:getpos() break end end end -- look at any players nearby, otherwise turn randomly if lp then local vec = { x = lp.x - s.x, y = lp.y - s.y, z = lp.z - s.z } if vec.x ~= 0 and vec.z ~= 0 then yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate if lp.x > s.x then yaw = yaw + pi end end else yaw = (math.random(0, 360) - 180) / 180 * pi end self.object:setyaw(yaw) end set_velocity(self, 0) set_animation(self, "stand") -- npc's ordered to stand stay standing if self.type ~= "npc" or self.order ~= "stand" then if self.walk_chance ~= 0 and math.random(1, 100) <= self.walk_chance and is_at_cliff(self) == false then set_velocity(self, self.walk_velocity) self.state = "walk" set_animation(self, "walk") end end elseif self.state == "walk" then local s = self.object:getpos() local lp = minetest.find_node_near(s, 1, {"group:water"}) -- water swimmers cannot move out of water if self.fly and self.fly_in == "default:water_source" and not lp then --print ("out of water") set_velocity(self, 0) -- change to undefined state so nothing more happens self.state = "flop" set_animation(self, "stand") return end -- if water nearby then turn away if lp then local vec = { x = lp.x - s.x, y = lp.y - s.y, z = lp.z - s.z } if vec.x ~= 0 and vec.z ~= 0 then yaw = math.atan(vec.z / vec.x) + 3 * pi / 2 - self.rotate if lp.x > s.x then yaw = yaw + pi end self.object:setyaw(yaw) end -- otherwise randomly turn elseif math.random(1, 100) <= 30 then yaw = (math.random(0, 360) - 180) / 180 * pi self.object:setyaw(yaw) end -- stand for great fall in front local temp_is_cliff = is_at_cliff(self) -- jump when walking comes to a halt if temp_is_cliff == false and self.jump and get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0 then do_jump(self) end if temp_is_cliff or math.random(1, 100) <= 30 then set_velocity(self, 0) self.state = "stand" set_animation(self, "stand") else set_velocity(self, self.walk_velocity) set_animation(self, "walk") end -- runaway when punched elseif self.state == "runaway" then self.runaway_timer = self.runaway_timer + 1 -- stop after 3 seconds or when at cliff if self.runaway_timer > 3 or is_at_cliff(self) then self.runaway_timer = 0 set_velocity(self, 0) self.state = "stand" set_animation(self, "stand") else set_velocity(self, self.run_velocity) set_animation(self, "walk") end -- jump when walking comes to a halt if self.jump and get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0 then do_jump(self) end -- attack routines (explode, dogfight, shoot, dogshoot) elseif self.state == "attack" then -- calculate distance from mob and enemy local s = self.object:getpos() local p = self.attack:getpos() or s local dist = get_distance(p, s) -- stop attacking if player or out of range if dist > self.view_range or not self.attack or not self.attack:getpos() or self.attack:get_hp() <= 0 then --print(" ** stop attacking **", dist, self.view_range) self.state = "stand" set_velocity(self, 0) set_animation(self, "stand") self.attack = nil self.v_start = false self.timer = 0 self.blinktimer = 0 return end if self.attack_type == "explode" then local vec = { x = p.x - s.x, y = p.y - s.y, z = p.z - s.z } if vec.x ~= 0 and vec.z ~= 0 then yaw = math.atan(vec.z / vec.x) + pi / 2 - self.rotate if p.x > s.x then yaw = yaw + pi end self.object:setyaw(yaw) end if dist > self.reach then if not self.v_start then self.v_start = true set_velocity(self, self.run_velocity) self.timer = 0 self.blinktimer = 0 else self.timer = 0 self.blinktimer = 0 if get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0 then local v = self.object:getvelocity() v.y = 5 self.object:setvelocity(v) end set_velocity(self, self.run_velocity) end set_animation(self, "run") else set_velocity(self, 0) self.timer = self.timer + dtime self.blinktimer = (self.blinktimer or 0) + dtime if self.blinktimer > 0.2 then self.blinktimer = 0 if self.blinkstatus then self.object:settexturemod("") else self.object:settexturemod("^[brighten") end self.blinkstatus = not self.blinkstatus end if self.timer > 3 then local pos = self.object:getpos() -- hurt player/mobs caught in blast area entity_physics(pos, 3) -- dont damage anything if area protected or next to water if minetest.find_node_near(pos, 1, {"group:water"}) or minetest.is_protected(pos, "") then if self.sounds.explode then minetest.sound_play(self.sounds.explode, { object = self.object, gain = 1.0, max_hear_distance = 16 }) end self.object:remove() effect(pos, 15, "tnt_smoke.png", 5) return end pos.y = pos.y - 1 mobs:explosion(pos, 2, 0, 1, self.sounds.explode) self.object:remove() return end end elseif self.attack_type == "dogfight" or (self.attack_type == "dogshoot" and dist <= self.reach) then if self.fly and dist > self.reach then local nod = node_ok(s) local p1 = s local me_y = math.floor(p1.y) local p2 = p local p_y = math.floor(p2.y + 1) local v = self.object:getvelocity() if nod.name == self.fly_in then if me_y < p_y then self.object:setvelocity({ x = v.x, y = 1 * self.walk_velocity, z = v.z }) elseif me_y > p_y then self.object:setvelocity({ x = v.x, y = -1 * self.walk_velocity, z = v.z }) end else if me_y < p_y then self.object:setvelocity({ x = v.x, y = 0.01, z = v.z }) elseif me_y > p_y then self.object:setvelocity({ x = v.x, y = -0.01, z = v.z }) end end end -- rnd: new movement direction if self.path.stuck and self.path.way then if #self.path.way>50 then self.path.stuck = false return end -- no paths longer than 50 local p1 = self.path.way[1]; if not p1 then self.path.stuck = false return end if math.abs(p1.x-s.x)+math.abs(p1.z-s.z)<0.6 then -- reached waypoint, remove it from queue table.remove(self.path.way,1); end p = {x=p1.x,y=p1.y,z=p1.z}; -- set new temporary target end local vec = { x = p.x - s.x, y = p.y - s.y, z = p.z - s.z } if vec.x ~= 0 and vec.z ~= 0 then yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate if p.x > s.x then yaw = yaw + pi end self.object:setyaw(yaw) -- rnd: look toward target end -- move towards enemy if beyond mob reach if dist > self.reach then -- PATH FINDING by rnd if enable_pathfinding then local s1 = self.path.lastpos; if math.abs(s1.x-s.x)+math.abs(s1.z-s.z)<1. then -- is it becoming stuck? 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}; if (self.path.stuck_timer>stuck_timeout and not self.path.stuck) or (self.path.stuck_timer>stuck_path_timeout and self.path.stuck) then -- im stuck, search for path self.path.stuck = true; local sheight=self.collisionbox[5]-self.collisionbox[2]; s.x=math.floor(s.x+0.5);s.y=math.floor(s.y+0.5)-sheight;s.z=math.floor(s.z+0.5); -- round position to center of node to avoid stuck in walls, also adjust height for player models! local ssight,sground;ssight,sground=minetest.line_of_sight(s, {x=s.x,y=s.y-4,z=s.z}, 1); if not ssight then s.y=sground.y+1 end-- determine node above ground --minetest.chat_send_all("stuck at " .. s.x .." " .. s.y .. " " .. s.z .. ", calculating path") local p1 = self.attack:getpos();p1.x=math.floor(p1.x+0.5);p1.y=math.floor(p1.y+0.5);p1.z=math.floor(p1.z+0.5); --minetest.find_path(pos1, pos2, searchdistance, max_jump, max_drop, algorithm) self.path.way = minetest.find_path(s, p1, 16, 2, 6,"Dijkstra"); --"A*_noprefetch"); if not self.path.way then self.path.stuck=false if enable_pathfind_digging then -- lets make way by digging/building if not accessible if s.y