diff --git a/mods/mobs/api.lua b/mods/mobs/api.lua index 2672036..1b6a46e 100644 --- a/mods/mobs/api.lua +++ b/mods/mobs/api.lua @@ -96,1171 +96,1179 @@ function mobs:register_mob(name, def) on_replace = def.on_replace or nil, on_spawn = def.on_spawn or nil, check_fed = function(self, name, feed_count, breed) -- check if a mob is fed - self.food = (self.food or 0) + 1 + self.food = (self.food or 0) + 1 - -- make children grow quicker - if self.child == true then - self.hornytimer = self.hornytimer + 20 - return true - end + -- make children grow quicker + if self.child == true then + self.hornytimer = self.hornytimer + 20 + return true + end - -- feed and tame - if self.food >= feed_count then - self.food = 0 + -- feed and tame + if self.food >= feed_count then + self.food = 0 - if breed and self.hornytimer == 0 then - self.horny = true - end + if breed and self.hornytimer == 0 then + self.horny = true + end - self.gotten = false + self.gotten = false - if (not self.tamed) and name ~= nil and (not self.owner) or self.owner == "" then - self.owner = name - self.tamed = true - end + if (not self.tamed) and name ~= nil and (not self.owner) or self.owner == "" then + self.owner = name + self.tamed = true + end - -- make sound when fed so many times - if self.sounds.random then - minetest.sound_play( - self.sounds.random, - { - object = self.object, - max_hear_distance = self.sounds.distance - }) - end - end - end, + -- make sound when fed so many times + if self.sounds.random then + minetest.sound_play( + self.sounds.random, + { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + end + end, do_attack = function(self, player, dist) - if is_too_near_spawn(self.object:getpos()) then return end + if is_too_near_spawn(self.object:getpos()) then return end - 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 = player - self.attack.dist = dist - end - end, + 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 = player + self.attack.dist = dist + end + end, set_velocity = function(self, v) - v = (v or 0) - if def.drawtype - and def.drawtype == "side" then - self.rotate = math.rad(90) - end - local yaw = self.object:getyaw() + self.rotate - local x = math.sin(yaw) * -v - local z = math.cos(yaw) * v - self.object:setvelocity({x = x, y = self.object:getvelocity().y, z = z}) - end, + v = (v or 0) + + if def.drawtype + and def.drawtype == "side" then + self.rotate = math.rad(90) + end + + local tmpyaw = self.object:getyaw() + local yaw = self.rotate; + + if tmpyaw ~= nan then + yaw = yaw + tmpyaw + else + minetest.log("object:getyaw() nan shim used") + end + + local x = math.sin(yaw) * -v + local z = math.cos(yaw) * v + + self.object:setvelocity({x = x, y = self.object:getvelocity().y, z = z}) + end, get_velocity = function(self) - local v = self.object:getvelocity() - return (v.x ^ 2 + v.z ^ 2) ^ (0.5) - end, + local v = self.object:getvelocity() + return (v.x ^ 2 + v.z ^ 2) ^ (0.5) + end, --[[ - in_fov = function(self,pos) - -- checks if POS is in self's FOV - local yaw = self.object:getyaw() + self.rotate - local vx = math.sin(yaw) - local vz = math.cos(yaw) - local ds = math.sqrt(vx ^ 2 + vz ^ 2) - local ps = math.sqrt(pos.x ^ 2 + pos.z ^ 2) - local d = { x = vx / ds, z = vz / ds } - local p = { x = pos.x / ps, z = pos.z / ps } - local an = ( d.x * p.x ) + ( d.z * p.z ) + in_fov = function(self,pos) + -- checks if POS is in self's FOV + local yaw = self.object:getyaw() + self.rotate + local vx = math.sin(yaw) + local vz = math.cos(yaw) + local ds = math.sqrt(vx ^ 2 + vz ^ 2) + local ps = math.sqrt(pos.x ^ 2 + pos.z ^ 2) + local d = { x = vx / ds, z = vz / ds } + local p = { x = pos.x / ps, z = pos.z / ps } + local an = ( d.x * p.x ) + ( d.z * p.z ) - a = math.deg( math.acos( an ) ) + a = math.deg( math.acos( an ) ) - if a > ( self.fov / 2 ) then - return false - else - return true - end - end,--]] + if a > ( self.fov / 2 ) then + return false + else + return true + end + end,--]] set_animation = function(self, type) - if not self.animation then - return - end + if not self.animation then + return + end - if not self.animation.current then - self.animation.current = "" - end + if not self.animation.current then + self.animation.current = "" + end - 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, + 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, on_step = function(self, dtime) - if self.type == "monster" - and peaceful_only then - self.object:remove() - return - end - - -- if lifetimer run out and not npc; tamed or attacking then remove mob - if self.type ~= "npc" - and not self.tamed then - self.lifetimer = self.lifetimer - dtime - if self.lifetimer <= 0 - and self.state ~= "attack" then - minetest.log("action","lifetimer expired, removed "..self.name) - effect(self.object:getpos(), 15, "tnt_smoke.png") - self.object:remove() - return - end - end - - -- check for mob drop/replace (used for chicken egg and sheep eating grass/wheat) - 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 - --and self.state == "stand" then - if self.on_replace ~= nil then - self.on_replace(self, pos) - else - minetest.set_node(pos, {name = self.replace_with}) - end - end - end - - local yaw = 0 - - if not self.fly then - -- floating in water (or falling) - local pos = self.object:getpos() - local nod = minetest.get_node_or_nil(pos) - if nod then nod = nod.name else nod = "default:dirt" end - local nodef = minetest.registered_nodes[nod] - - local v = self.object:getvelocity() - if v.y > 0.1 then - self.object:setacceleration({ - x = 0, - y= self.fall_speed, - z = 0 - }) - end - if nodef.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 - 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 or 0) - self.object:getpos().y - if d > 5 then - self.object:set_hp(self.object:get_hp() - math.floor(d - 5)) - effect(self.object:getpos(), 5, "tnt_smoke.png") - check_for_death(self) - 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 - - 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 - - local do_env_damage = function(self) - - local pos = self.object:getpos() - local tod = minetest.get_timeofday() - - -- daylight above ground - if self.light_damage ~= 0 - and pos.y > 0 - and tod > 0.2 - and tod < 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") - if check_for_death(self) then return end - end - - pos.y = pos.y + self.collisionbox[2] -- foot level - local nod = minetest.get_node_or_nil(pos) - if not nod then return end ; -- 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") - if check_for_death(self) then return end - end - - -- lava or fire - if self.lava_damage ~= 0 - and nodef.groups.lava then - self.object:set_hp(self.object:get_hp() - self.lava_damage) - effect(pos, 5, "mobs_flame.png", 8) - if check_for_death(self) then return end - end - - end - - local do_jump = function(self) - if self.fly then - return - end - - self.jumptimer = (self.jumptimer or 0) + 1 - if self.jumptimer < 3 then - local pos = self.object:getpos() - pos.y = (pos.y + self.collisionbox[2]) - 0.2 - local nod = minetest.get_node(pos) - --print ("standing on:", nod.name, pos.y) - if not nod - or not minetest.registered_nodes[nod.name] - or minetest.registered_nodes[nod.name].walkable == false then - return - end - if self.direction then - pos.y = pos.y + 0.5 - local nod = minetest.get_node_or_nil( - { - x = pos.x + self.direction.x, - y = pos.y, - z = pos.z + self.direction.z - }) - --print ("in front:", nod.name, pos.y) - if nod and nod.name and - (nod.name ~= "air" - or self.walk_chance == 0) then - local def = minetest.registered_items[nod.name] - if (def - and def.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, - max_hear_distance = self.sounds.distance - }) - end - end - end - end - else - self.jumptimer = 0 - end - end - - -- environmental damage timer - self.env_damage_timer = self.env_damage_timer + dtime - if self.state == "attack" - and self.env_damage_timer > 1 then - self.env_damage_timer = 0 - do_env_damage(self) - elseif self.state ~= "attack" then - do_env_damage(self) - end - - -- find someone to attack - if self.type == "monster" - and damage_enabled - and self.state ~= "attack" 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 ipairs(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 - p.y = p.y + 1 - sp.y = sp.y + 1 -- aim higher to make looking up hills more realistic - dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - if dist < self.view_range then - -- and self.in_fov(self,p) then - -- 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 - self.do_attack(self, min_player, min_dist) - 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 = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - if dist < min_dist then - min_dist = dist - min_player = obj.object - end - end - end - if min_player then - self.do_attack(self, min_player, min_dist) - end - 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 - and self.child == false then - self.hornytimer = self.hornytimer + 1 - if self.hornytimer >= 240 then - self.hornytimer = 0 - self.horny = false - end - end - - -- if animal is 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, - }) - end - end - - -- if animal is horny, find another same animal who is horny and mate - if self.horny == true - and self.hornytimer <= 40 then - local pos = self.object:getpos() - effect({x = pos.x, y = pos.y + 0.5, z = pos.z}, 4, "heart.png") - local ents = minetest.get_objects_inside_radius(pos, self.view_range) - local num = 0 - local ent = nil - for i,obj in ipairs(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 - if num > 1 then - self.hornytimer = 41 - ent.hornytimer = 41 - minetest.after( - 7, - function(dtime) - local mob = minetest.add_entity(pos, self.name) - local ent2 = mob:get_luaentity() - local textures = self.base_texture - if def.child_texture then - textures = def.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.following = ent -- follow mother - end) - num = 0 - break - end - end - end - - -- find player to follow - if (self.follow ~= "" or self.order == "follow") and not self.following and self.state ~= "attack" then - local s, p, dist - for _,player in pairs(minetest.get_connected_players()) do - s = self.object:getpos() - p = player:getpos() - dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - if dist < self.view_range then - self.following = player - break - end - end - end - - -- custom function (defined in mob lua file) - if self.do_custom then - self.do_custom(self) - end - - if self.type == "npc" - and self.order == "follow" - and self.state ~= "attack" then - -- npc stop following player if not owner - if self.following - and self.type == "npc" - 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 self.following:get_wielded_item():get_name() ~= self.follow then - self.following = nil - end - end - - -- follow player or mob - if self.following then - local s = self.object:getpos() - local p - - if self.following.is_player - and 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 = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - 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} - yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate - if p.x > s.x then - yaw = yaw + math.pi - end - self.object:setyaw(yaw) - - -- anyone but standing npc's can move along - if dist > 2 and self.order ~= "stand" then - if (self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0) or (self.object:getvelocity().y == 0 and self.jump_chance > 0) then - self.direction = { - x = math.sin(yaw) * -1, - y = -20, - z = math.cos(yaw) - } - do_jump(self) - end - self.set_velocity(self, self.walk_velocity) - if self.walk_chance ~= 0 then - self:set_animation("walk") - end - else - self.set_velocity(self, 0) - self:set_animation("stand") - end - return - end - end - else - self:set_animation("stand") - end - - if self.state == "stand" then - -- randomly turn - if math.random(1, 4) == 1 then - -- if there is a player nearby look at them - local lp = nil - local s = self.object:getpos() - - if self.type == "npc" then - local o = minetest.get_objects_inside_radius(self.object:getpos(), 3) - - local yaw = 0 - for _,o in ipairs(o) do - if o:is_player() then - lp = o:getpos() - break - end - end - end - - if lp ~= nil then - local vec = {x = lp.x - s.x, y = lp.y - s.y, z = lp.z - s.z} - yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate - if lp.x > s.x then - yaw = yaw + math.pi - end - else - yaw = self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * math.pi) - end - self.object:setyaw(yaw) - end - - self.set_velocity(self, 0) - self.set_animation(self, "stand") - - -- npc's ordered to stand stay standing - if self.type == "npc" - and self.order == "stand" then - self.set_velocity(self, 0) - self.state = "stand" - self:set_animation("stand") - else - if self.walk_chance ~= 0 and math.random(1, 100) <= self.walk_chance then - self.set_velocity(self, self.walk_velocity) - self.state = "walk" - self.set_animation(self, "walk") - end - - -- jumping mobs only - -- if self.jump and math.random(1, 100) <= self.jump_chance then - -- self.direction = {x = 0, y = 0, z = 0} - -- do_jump(self) - -- self.set_velocity(self, self.walk_velocity) - -- 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") - self.set_velocity(self, 0) - self.state = "flop" -- change to undefined state so nothing more happens - self:set_animation("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} - yaw = math.atan(vec.z / vec.x) + 3 * math.pi / 2 - self.rotate - if lp.x > s.x then - yaw = yaw + math.pi - end - self.object:setyaw(yaw) - - -- otherwise randomly turn - elseif math.random(1, 100) <= 30 then - self.object:setyaw(self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * math.pi)) - end - if self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0 then - self.direction = { - x = math.sin(yaw) * -1, - y = -20, - z = math.cos(yaw) - } - do_jump(self) - end - - self:set_animation("walk") - self.set_velocity(self, self.walk_velocity) - if math.random(1, 100) <= 30 then - self.set_velocity(self, 0) - self.state = "stand" - self:set_animation("stand") - end - - -- exploding mobs - elseif self.state == "attack" and self.attack_type == "explode" then - if not self.attack.player - or not self.attack.player:is_player() then - self.state = "stand" - self:set_animation("stand") - self.timer = 0 - self.blinktimer = 0 - return - end - local s = self.object:getpos() - local p = self.attack.player:getpos() - local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - if dist > self.view_range or self.attack.player:get_hp() <= 0 then - self.state = "stand" - self.v_start = false - self.set_velocity(self, 0) - self.timer = 0 - self.blinktimer = 0 - self.attack = {player = nil, dist = nil} - self:set_animation("stand") - return - else - self:set_animation("walk") - self.attack.dist = dist - end - - local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} - yaw = math.atan(vec.z / vec.x) + math.pi / 2 - self.rotate - if p.x > s.x then - yaw = yaw+math.pi - end - self.object:setyaw(yaw) - if self.attack.dist > 3 then - if not self.v_start then - self.v_start = true - self.set_velocity(self, self.run_velocity) - self.timer = 0 - self.blinktimer = 0 - else - self.timer = 0 - self.blinktimer = 0 - if self.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 - self.set_velocity(self, self.run_velocity) - end - self:set_animation("run") - else - self.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 = vector.round(self.object:getpos()) - entity_physics(pos, 3) -- hurt player/mobs caught in blast area - if minetest.find_node_near(pos, 1, {"group:water"}) or minetest.is_protected(pos, "") then - self.object:remove() - if self.sounds.explode ~= "" then - minetest.sound_play(self.sounds.explode, { - pos = pos, - gain = 1.0, - max_hear_distance = 16 - }) - end - effect(pos, 15, "tnt_smoke.png", 5) - return - end - self.object:remove() - pos.y = pos.y - 1 - mobs:explosion(pos, self.explode_radius, 0, 1, self.sounds.explode) - end - end - -- end of exploding mobs - - elseif self.state == "attack" - and self.attack_type == "dogfight" then - if not self.attack.player or not self.attack.player:getpos() then - print("stop attacking") - self.state = "stand" - self:set_animation("stand") - return - end - local s = self.object:getpos() - local p = self.attack.player:getpos() - local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - - -- fly bit modified from BlockMens creatures mod - if self.fly - and dist > 2 then - - local nod = minetest.get_node_or_nil(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 - and 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 - -- end fly bit - - if dist > self.view_range or self.attack.player:get_hp() <= 0 then - self.state = "stand" - self.set_velocity(self, 0) - self.attack = {player = nil, dist = nil} - self:set_animation("stand") - return - else - self.attack.dist = dist - end - - local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} - yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate - if p.x > s.x then - yaw = yaw + math.pi - end - self.object:setyaw(yaw) - -- attack distance is 2 + half of mob width so the bigger mobs can attack (like slimes) - if self.attack.dist > ((-self.collisionbox[1] + self.collisionbox[4]) / 2) + 2 then - -- jump attack - if (self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0) or (self.object:getvelocity().y == 0 and self.jump_chance > 0) then - self.direction = { - x = math.sin(yaw) * -1, - y = -20, - z = math.cos(yaw) - } - do_jump(self) - end - self.set_velocity(self, self.run_velocity) - self:set_animation("run") - else - self.set_velocity(self, 0) - self:set_animation("punch") - if self.timer > 1 then - self.timer = 0 - local p2 = p - local s2 = s - p2.y = p2.y + 1.5 - s2.y = s2.y + 1.5 - if minetest.line_of_sight(p2, s2) == true then - if self.sounds.attack then - minetest.sound_play( - self.sounds.attack, - { - object = self.object, - max_hear_distance = self.sounds.distance - }) - end - self.attack.player:punch( - self.object, - 1.0, - { - full_punch_interval=1.0, - damage_groups = {fleshy=self.damage} - }, vec) - if self.attack.player:get_hp() <= 0 then - self.state = "stand" - self:set_animation("stand") - end - end - end - end - - elseif self.state == "attack" - and self.attack_type == "shoot" then - - local s = self.object:getpos() - local p = self.attack.player:getpos() - if not p then - self.state = "stand" - return - end - p.y = p.y - .5 - s.y = s.y + .5 - local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 - if dist > self.view_range or self.attack.player:get_hp() <= 0 then - self.state = "stand" - self.set_velocity(self, 0) - self:set_animation("stand") - return - else - self.attack.dist = dist - end - - local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} - yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate - if p.x > s.x then - yaw = yaw + math.pi - end - self.object:setyaw(yaw) - self.set_velocity(self, 0) - - if self.shoot_interval and self.timer > self.shoot_interval and math.random(1, 100) <= 60 then - self.timer = 0 - - self:set_animation("punch") - - if self.sounds.attack then - minetest.sound_play( - self.sounds.attack, - { - object = self.object, - max_hear_distance = self.sounds.distance - }) - end - - local p = self.object:getpos() - p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) / 2 - local obj = minetest.add_entity(p, self.arrow) - local amount = (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 - local v = obj:get_luaentity().velocity - vec.y = vec.y + self.shoot_offset -- this makes shoot aim accurate - vec.x = vec.x *v / amount - vec.y = vec.y *v / amount - vec.z = vec.z *v / amount - obj:setvelocity(vec) - end - end - end, - - on_activate = function(self, staticdata, dtime_s) - - if self.type == "monster" - and peaceful_only then - self.object:remove() - end - - -- load entity variables - if staticdata then - local tmp = minetest.deserialize(staticdata) - if tmp then - for _,stat in pairs(tmp) do - self[_] = stat - end - end - end - - -- select random texture, set model and size - if not self.base_texture then - self.base_texture = def.textures[math.random(1, #def.textures)] - self.base_mesh = def.mesh - self.base_size = self.visual_size - self.base_colbox = self.collisionbox - end - - -- set texture, model and size - local textures = self.base_texture - local mesh = self.base_mesh - local vis_size = self.base_size - local colbox = self.base_colbox - - -- specific texture if gotten - if self.gotten == true - and def.gotten_texture then - textures = def.gotten_texture - end - - -- specific mesh if gotten - if self.gotten == true - and def.gotten_mesh then - mesh = def.gotten_mesh - end - - -- if object is child then set half size - if self.child == true then - vis_size = { - x = self.base_size.x / 2, - y = self.base_size.y / 2 - } - if def.child_texture then - textures = def.child_texture[1] - end - colbox = { - 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 - } - end - - if self.health == 0 then - self.health = math.random (self.hp_min, self.hp_max) - end - - self.object:set_hp( self.health ) - self.object:set_armor_groups({fleshy = self.armor}) - self.state = "stand" - self.order = "stand" - self.following = nil - self.old_y = self.object:getpos().y - self.object:setyaw(math.random(1, 360) / 180 * math.pi) - self.sounds.distance = (self.sounds.distance or 10) - self.textures = textures - self.mesh = mesh - self.collisionbox = colbox - self.visual_size = vis_size - -- set anything changed above - self.object:set_properties(self) - - if self.on_spawn then - self:on_spawn() - end - - end, - - get_staticdata = function(self) - - -- remove mob when out of range unless tamed - if mobs.remove == true and self.remove_ok and not self.tamed then - print ("REMOVED", self.remove_ok, self.name) - self.object:remove() - end - self.remove_ok = true - self.attack = nil - - local tmp = {} - for _,stat in pairs(self) do - local t = type(stat) - if t ~= 'function' - and t ~= 'nil' - and t ~= 'userdata' then - tmp[_] = self[_] - end - end - -- print('===== '..self.name..'\n'.. dump(tmp)..'\n=====\n') - return minetest.serialize(tmp) - end, - - on_punch = function(self, hitter, tflp, tool_capabilities, dir) - -- weapon wear - local weapon = hitter:get_wielded_item() - if weapon:get_definition().tool_capabilities ~= nil then - local wear = ( (weapon:get_definition().tool_capabilities.full_punch_interval or 1.4) / 75 ) * 9000 - weapon:add_wear(wear) - hitter:set_wielded_item(weapon) - end - - -- 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=hitter, - max_hear_distance = 8 - }) - else - minetest.sound_play("default_punch", { - object = hitter, - max_hear_distance = 5 - }) - end - - -- exit here if dead - if check_for_death(self) then - return - end - - -- blood_particles - if self.blood_amount > 0 then - local pos = self.object:getpos() - pos.y = pos.y + (-self.collisionbox[2] + self.collisionbox[5]) / 3 - if enable_blood then - effect(pos, self.blood_amount, self.blood_texture) - else - effect(pos, self.blood_amount, "default_grass_clump_tall.png") - end - end - - -- knock back effect - if self.knock_back > 0 then - local kb = self.knock_back - local r = self.recovery_time - local v = self.object:getvelocity() - if tflp < tool_capabilities.full_punch_interval then - if kb > 0 then - kb = kb * ( tflp / tool_capabilities.full_punch_interval ) - end - r = r * ( tflp / tool_capabilities.full_punch_interval ) - end - self.object:setvelocity({x = dir.x * kb,y = 0,z = dir.z * kb}) - self.pause_timer = r - end - - -- attack puncher and call other mobs for help - if self.passive == false - and not self.tamed then - if self.state ~= "attack" then - self.do_attack(self, hitter, 1) - end - -- alert others to the attack - local obj = nil - for _, oir in pairs(minetest.get_objects_inside_radius(hitter:getpos(), 5)) do - obj = oir:get_luaentity() - if obj then - if obj.group_attack == true - and obj.state ~= "attack" then - obj.do_attack(obj, hitter, 1) - end - end - end - end - end, + if self.type == "monster" + and peaceful_only then + self.object:remove() + return + end + + -- if lifetimer run out and not npc; tamed or attacking then remove mob + if self.type ~= "npc" + and not self.tamed then + self.lifetimer = self.lifetimer - dtime + if self.lifetimer <= 0 + and self.state ~= "attack" then + minetest.log("action","lifetimer expired, removed "..self.name) + effect(self.object:getpos(), 15, "tnt_smoke.png") + self.object:remove() + return + end + end + + -- check for mob drop/replace (used for chicken egg and sheep eating grass/wheat) + 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 + --and self.state == "stand" then + if self.on_replace ~= nil then + self.on_replace(self, pos) + else + minetest.set_node(pos, {name = self.replace_with}) + end + end + end + + local yaw = 0 + + if not self.fly then + -- floating in water (or falling) + local pos = self.object:getpos() + local nod = minetest.get_node_or_nil(pos) + if nod then nod = nod.name else nod = "default:dirt" end + local nodef = minetest.registered_nodes[nod] + + local v = self.object:getvelocity() + if v.y > 0.1 then + self.object:setacceleration({ + x = 0, + y= self.fall_speed, + z = 0 + }) + end + if nodef.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 + 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 or 0) - self.object:getpos().y + if d > 5 then + self.object:set_hp(self.object:get_hp() - math.floor(d - 5)) + effect(self.object:getpos(), 5, "tnt_smoke.png") + check_for_death(self) + 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 + + 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 + + local do_env_damage = function(self) + + local pos = self.object:getpos() + local tod = minetest.get_timeofday() + + -- daylight above ground + if self.light_damage ~= 0 + and pos.y > 0 + and tod > 0.2 + and tod < 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") + if check_for_death(self) then return end + end + + pos.y = pos.y + self.collisionbox[2] -- foot level + local nod = minetest.get_node_or_nil(pos) + if not nod then return end ; -- 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") + if check_for_death(self) then return end + end + + -- lava or fire + if self.lava_damage ~= 0 + and nodef.groups.lava then + self.object:set_hp(self.object:get_hp() - self.lava_damage) + effect(pos, 5, "mobs_flame.png", 8) + if check_for_death(self) then return end + end + + end + + local do_jump = function(self) + if self.fly then + return + end + + self.jumptimer = (self.jumptimer or 0) + 1 + if self.jumptimer < 3 then + local pos = self.object:getpos() + pos.y = (pos.y + self.collisionbox[2]) - 0.2 + local nod = minetest.get_node(pos) + --print("standing on:", nod.name, pos.y) + if not nod + or not minetest.registered_nodes[nod.name] + or minetest.registered_nodes[nod.name].walkable == false then + return + end + if self.direction then + pos.y = pos.y + 0.5 + local nod = minetest.get_node_or_nil( + { + x = pos.x + self.direction.x, + y = pos.y, + z = pos.z + self.direction.z + }) + --print("in front:", nod.name, pos.y) + if nod and nod.name and + (nod.name ~= "air" + or self.walk_chance == 0) then + local def = minetest.registered_items[nod.name] + if (def + and def.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, + max_hear_distance = self.sounds.distance + }) + end + end + end + end + else + self.jumptimer = 0 + end + end + + -- environmental damage timer + self.env_damage_timer = self.env_damage_timer + dtime + if self.state == "attack" + and self.env_damage_timer > 1 then + self.env_damage_timer = 0 + do_env_damage(self) + elseif self.state ~= "attack" then + do_env_damage(self) + end + + -- find someone to attack + if self.type == "monster" + and damage_enabled + and self.state ~= "attack" 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 ipairs(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 + p.y = p.y + 1 + sp.y = sp.y + 1 -- aim higher to make looking up hills more realistic + dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + if dist < self.view_range then + -- and self.in_fov(self,p) then + -- 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 + self.do_attack(self, min_player, min_dist) + 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 = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + if dist < min_dist then + min_dist = dist + min_player = obj.object + end + end + end + if min_player then + self.do_attack(self, min_player, min_dist) + end + 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 + and self.child == false then + self.hornytimer = self.hornytimer + 1 + if self.hornytimer >= 240 then + self.hornytimer = 0 + self.horny = false + end + end + + -- if animal is 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, + }) + end + end + + -- if animal is horny, find another same animal who is horny and mate + if self.horny == true + and self.hornytimer <= 40 then + local pos = self.object:getpos() + effect({x = pos.x, y = pos.y + 0.5, z = pos.z}, 4, "heart.png") + local ents = minetest.get_objects_inside_radius(pos, self.view_range) + local num = 0 + local ent = nil + for i,obj in ipairs(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 + if num > 1 then + self.hornytimer = 41 + ent.hornytimer = 41 + minetest.after( + 7, + function(dtime) + local mob = minetest.add_entity(pos, self.name) + local ent2 = mob:get_luaentity() + local textures = self.base_texture + if def.child_texture then + textures = def.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.following = ent -- follow mother + end) + num = 0 + break + end + end + end + + -- find player to follow + if (self.follow ~= "" or self.order == "follow") and not self.following and self.state ~= "attack" then + local s, p, dist + for _,player in pairs(minetest.get_connected_players()) do + s = self.object:getpos() + p = player:getpos() + dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + if dist < self.view_range then + self.following = player + break + end + end + end + + -- custom function (defined in mob lua file) + if self.do_custom then + self.do_custom(self) + end + + if self.type == "npc" + and self.order == "follow" + and self.state ~= "attack" then + -- npc stop following player if not owner + if self.following + and self.type == "npc" + 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 self.following:get_wielded_item():get_name() ~= self.follow then + self.following = nil + end + end + + -- follow player or mob + if self.following then + local s = self.object:getpos() + local p + + if self.following.is_player + and 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 = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + 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} + yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + if p.x > s.x then + yaw = yaw + math.pi + end + self.object:setyaw(yaw) + + -- anyone but standing npc's can move along + if dist > 2 and self.order ~= "stand" then + if (self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0) or (self.object:getvelocity().y == 0 and self.jump_chance > 0) then + self.direction = { + x = math.sin(yaw) * -1, + y = -20, + z = math.cos(yaw) + } + do_jump(self) + end + self.set_velocity(self, self.walk_velocity) + if self.walk_chance ~= 0 then + self:set_animation("walk") + end + else + self.set_velocity(self, 0) + self:set_animation("stand") + end + return + end + end + else + self:set_animation("stand") + end + + if self.state == "stand" then + -- randomly turn + if math.random(1, 4) == 1 then + -- if there is a player nearby look at them + local lp = nil + local s = self.object:getpos() + + if self.type == "npc" then + local o = minetest.get_objects_inside_radius(self.object:getpos(), 3) + + local yaw = 0 + for _,o in ipairs(o) do + if o:is_player() then + lp = o:getpos() + break + end + end + end + + if lp ~= nil then + local vec = {x = lp.x - s.x, y = lp.y - s.y, z = lp.z - s.z} + yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + if lp.x > s.x then + yaw = yaw + math.pi + end + else + yaw = self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * math.pi) + end + self.object:setyaw(yaw) + end + + self.set_velocity(self, 0) + self.set_animation(self, "stand") + + -- npc's ordered to stand stay standing + if self.type == "npc" + and self.order == "stand" then + self.set_velocity(self, 0) + self.state = "stand" + self:set_animation("stand") + else + if self.walk_chance ~= 0 and math.random(1, 100) <= self.walk_chance then + self.set_velocity(self, self.walk_velocity) + self.state = "walk" + self.set_animation(self, "walk") + end + + -- jumping mobs only + -- if self.jump and math.random(1, 100) <= self.jump_chance then + -- self.direction = {x = 0, y = 0, z = 0} + -- do_jump(self) + -- self.set_velocity(self, self.walk_velocity) + -- 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") + self.set_velocity(self, 0) + self.state = "flop" -- change to undefined state so nothing more happens + self:set_animation("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} + yaw = math.atan(vec.z / vec.x) + 3 * math.pi / 2 - self.rotate + if lp.x > s.x then + yaw = yaw + math.pi + end + self.object:setyaw(yaw) + + -- otherwise randomly turn + elseif math.random(1, 100) <= 30 then + self.object:setyaw(self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * math.pi)) + end + if self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0 then + self.direction = { + x = math.sin(yaw) * -1, + y = -20, + z = math.cos(yaw) + } + do_jump(self) + end + + self:set_animation("walk") + self.set_velocity(self, self.walk_velocity) + if math.random(1, 100) <= 30 then + self.set_velocity(self, 0) + self.state = "stand" + self:set_animation("stand") + end + + -- exploding mobs + elseif self.state == "attack" and self.attack_type == "explode" then + if not self.attack.player + or not self.attack.player:is_player() then + self.state = "stand" + self:set_animation("stand") + self.timer = 0 + self.blinktimer = 0 + return + end + local s = self.object:getpos() + local p = self.attack.player:getpos() + local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + if dist > self.view_range or self.attack.player:get_hp() <= 0 then + self.state = "stand" + self.v_start = false + self.set_velocity(self, 0) + self.timer = 0 + self.blinktimer = 0 + self.attack = {player = nil, dist = nil} + self:set_animation("stand") + return + else + self:set_animation("walk") + self.attack.dist = dist + end + + local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} + yaw = math.atan(vec.z / vec.x) + math.pi / 2 - self.rotate + if p.x > s.x then + yaw = yaw+math.pi + end + self.object:setyaw(yaw) + if self.attack.dist > 3 then + if not self.v_start then + self.v_start = true + self.set_velocity(self, self.run_velocity) + self.timer = 0 + self.blinktimer = 0 + else + self.timer = 0 + self.blinktimer = 0 + if self.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 + self.set_velocity(self, self.run_velocity) + end + self:set_animation("run") + else + self.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 = vector.round(self.object:getpos()) + entity_physics(pos, 3) -- hurt player/mobs caught in blast area + if minetest.find_node_near(pos, 1, {"group:water"}) or minetest.is_protected(pos, "") then + self.object:remove() + if self.sounds.explode ~= "" then + minetest.sound_play(self.sounds.explode, { + pos = pos, + gain = 1.0, + max_hear_distance = 16 + }) + end + effect(pos, 15, "tnt_smoke.png", 5) + return + end + self.object:remove() + pos.y = pos.y - 1 + mobs:explosion(pos, self.explode_radius, 0, 1, self.sounds.explode) + end + end + -- end of exploding mobs + + -- print("stop attacking") + self.state = "stand" + self:set_animation("stand") + return + end + local s = self.object:getpos() + local p = self.attack.player:getpos() + local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + + -- fly bit modified from BlockMens creatures mod + if self.fly + and dist > 2 then + + local nod = minetest.get_node_or_nil(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 + and 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 + -- end fly bit + + if dist > self.view_range or self.attack.player:get_hp() <= 0 then + self.state = "stand" + self.set_velocity(self, 0) + self.attack = {player = nil, dist = nil} + self:set_animation("stand") + return + else + self.attack.dist = dist + end + + local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} + yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + if p.x > s.x then + yaw = yaw + math.pi + end + self.object:setyaw(yaw) + -- attack distance is 2 + half of mob width so the bigger mobs can attack (like slimes) + if self.attack.dist > ((-self.collisionbox[1] + self.collisionbox[4]) / 2) + 2 then + -- jump attack + if (self.jump and self.get_velocity(self) <= 0.5 and self.object:getvelocity().y == 0) or (self.object:getvelocity().y == 0 and self.jump_chance > 0) then + self.direction = { + x = math.sin(yaw) * -1, + y = -20, + z = math.cos(yaw) + } + do_jump(self) + end + self.set_velocity(self, self.run_velocity) + self:set_animation("run") + else + self.set_velocity(self, 0) + self:set_animation("punch") + if self.timer > 1 then + self.timer = 0 + local p2 = p + local s2 = s + p2.y = p2.y + 1.5 + s2.y = s2.y + 1.5 + if minetest.line_of_sight(p2, s2) == true then + if self.sounds.attack then + minetest.sound_play( + self.sounds.attack, + { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + self.attack.player:punch( + self.object, + 1.0, + { + full_punch_interval=1.0, + damage_groups = {fleshy=self.damage} + }, vec) + if self.attack.player:get_hp() <= 0 then + self.state = "stand" + self:set_animation("stand") + end + end + end + end + + elseif self.state == "attack" + and self.attack_type == "shoot" then + + local s = self.object:getpos() + local p = self.attack.player:getpos() + if not p then + self.state = "stand" + return + end + p.y = p.y - .5 + s.y = s.y + .5 + local dist = ((p.x - s.x) ^ 2 + (p.y - s.y) ^ 2 + (p.z - s.z) ^ 2) ^ 0.5 + if dist > self.view_range or self.attack.player:get_hp() <= 0 then + self.state = "stand" + self.set_velocity(self, 0) + self:set_animation("stand") + return + else + self.attack.dist = dist + end + + local vec = {x = p.x - s.x, y = p.y - s.y, z = p.z - s.z} + yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + if p.x > s.x then + yaw = yaw + math.pi + end + self.object:setyaw(yaw) + self.set_velocity(self, 0) + + if self.shoot_interval and self.timer > self.shoot_interval and math.random(1, 100) <= 60 then + self.timer = 0 + + self:set_animation("punch") + + if self.sounds.attack then + minetest.sound_play( + self.sounds.attack, + { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + local p = self.object:getpos() + p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) / 2 + local obj = minetest.add_entity(p, self.arrow) + local amount = (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 + local v = obj:get_luaentity().velocity + vec.y = vec.y + self.shoot_offset -- this makes shoot aim accurate + vec.x = vec.x *v / amount + vec.y = vec.y *v / amount + vec.z = vec.z *v / amount + obj:setvelocity(vec) + end + end +end, + +on_activate = function(self, staticdata, dtime_s) + + if self.type == "monster" + and peaceful_only then + self.object:remove() + end + + -- load entity variables + if staticdata then + local tmp = minetest.deserialize(staticdata) + if tmp then + for _,stat in pairs(tmp) do + self[_] = stat + end + end + end + + -- select random texture, set model and size + if not self.base_texture then + self.base_texture = def.textures[math.random(1, #def.textures)] + self.base_mesh = def.mesh + self.base_size = self.visual_size + self.base_colbox = self.collisionbox + end + + -- set texture, model and size + local textures = self.base_texture + local mesh = self.base_mesh + local vis_size = self.base_size + local colbox = self.base_colbox + + -- specific texture if gotten + if self.gotten == true + and def.gotten_texture then + textures = def.gotten_texture + end + + -- specific mesh if gotten + if self.gotten == true + and def.gotten_mesh then + mesh = def.gotten_mesh + end + + -- if object is child then set half size + if self.child == true then + vis_size = { + x = self.base_size.x / 2, + y = self.base_size.y / 2 + } + if def.child_texture then + textures = def.child_texture[1] + end + colbox = { + 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 + } + end + + if self.health == 0 then + self.health = math.random (self.hp_min, self.hp_max) + end + + self.object:set_hp( self.health ) + self.object:set_armor_groups({fleshy = self.armor}) + self.state = "stand" + self.order = "stand" + self.following = nil + self.old_y = self.object:getpos().y + self.object:setyaw(math.random(1, 360) / 180 * math.pi) + self.sounds.distance = (self.sounds.distance or 10) + self.textures = textures + self.mesh = mesh + self.collisionbox = colbox + self.visual_size = vis_size + -- set anything changed above + self.object:set_properties(self) + + if self.on_spawn then + self:on_spawn() + end + +end, + +get_staticdata = function(self) + + -- remove mob when out of range unless tamed + if mobs.remove == true and self.remove_ok and not self.tamed then + print ("REMOVED", self.remove_ok, self.name) + self.object:remove() + end + self.remove_ok = true + self.attack = nil + + local tmp = {} + for _,stat in pairs(self) do + local t = type(stat) + if t ~= 'function' + and t ~= 'nil' + and t ~= 'userdata' then + tmp[_] = self[_] + end + end + -- print('===== '..self.name..'\n'.. dump(tmp)..'\n=====\n') + return minetest.serialize(tmp) +end, + +on_punch = function(self, hitter, tflp, tool_capabilities, dir) + -- weapon wear + local weapon = hitter:get_wielded_item() + if weapon:get_definition().tool_capabilities ~= nil then + local wear = ( (weapon:get_definition().tool_capabilities.full_punch_interval or 1.4) / 75 ) * 9000 + weapon:add_wear(wear) + hitter:set_wielded_item(weapon) + end + + -- 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=hitter, + max_hear_distance = 8 }) + else + minetest.sound_play("default_punch", { + object = hitter, + max_hear_distance = 5 + }) + end + + -- exit here if dead + if check_for_death(self) then + return + end + + -- blood_particles + if self.blood_amount > 0 then + local pos = self.object:getpos() + pos.y = pos.y + (-self.collisionbox[2] + self.collisionbox[5]) / 3 + if enable_blood then + effect(pos, self.blood_amount, self.blood_texture) + else + effect(pos, self.blood_amount, "default_grass_clump_tall.png") + end + end + + -- knock back effect + if self.knock_back > 0 then + local kb = self.knock_back + local r = self.recovery_time + local v = self.object:getvelocity() + if tflp < tool_capabilities.full_punch_interval then + if kb > 0 then + kb = kb * ( tflp / tool_capabilities.full_punch_interval ) + end + r = r * ( tflp / tool_capabilities.full_punch_interval ) + end + self.object:setvelocity({x = dir.x * kb,y = 0,z = dir.z * kb}) + self.pause_timer = r + end + + -- attack puncher and call other mobs for help + if self.passive == false + and not self.tamed then + if self.state ~= "attack" then + self.do_attack(self, hitter, 1) + end + -- alert others to the attack + local obj = nil + for _, oir in pairs(minetest.get_objects_inside_radius(hitter:getpos(), 5)) do + obj = oir:get_luaentity() + if obj then + if obj.group_attack == true + and obj.state ~= "attack" then + obj.do_attack(obj, hitter, 1) + end + end + end + end +end, +}) end mobs.spawning_mobs = {} @@ -1274,61 +1282,61 @@ function mobs:spawn_specific(name, nodes, neighbors, min_light, max_light, inter interval = interval, chance = chance, action = function(pos, node, _, active_object_count_wider) - -- do not spawn if too many active entities in area - if active_object_count_wider > active_object_count or not mobs.spawning_mobs[name] then - --or not pos then - return - end + -- do not spawn if too many active entities in area + if active_object_count_wider > active_object_count or not mobs.spawning_mobs[name] then + --or not pos then + return + end - -- spawn above node - pos.y = pos.y + 1 + -- spawn above node + pos.y = pos.y + 1 - -- mobs cannot spawn inside protected areas if enabled - if mobs.protected == 1 - and minetest.is_protected(pos, "") then - return - end + -- mobs cannot spawn inside protected areas if enabled + if mobs.protected == 1 + and minetest.is_protected(pos, "") then + return + end - -- check if light and height levels are ok to spawn - local light = minetest.get_node_light(pos) - if not light - or light > max_light - or light < min_light - or pos.y > max_height - or pos.y < min_height then - return - end + -- check if light and height levels are ok to spawn + local light = minetest.get_node_light(pos) + if not light + or light > max_light + or light < min_light + or pos.y > max_height + or pos.y < min_height then + return + end - -- are we spawning inside a solid node? - local nod = minetest.get_node_or_nil(pos) - if not nod - or not nod.name - or not minetest.registered_nodes[nod.name] - or minetest.registered_nodes[nod.name].walkable == true then - return - end + -- are we spawning inside a solid node? + local nod = minetest.get_node_or_nil(pos) + if not nod + or not nod.name + or not minetest.registered_nodes[nod.name] + or minetest.registered_nodes[nod.name].walkable == true then + return + end - pos.y = pos.y + 1 + pos.y = pos.y + 1 - nod = minetest.get_node_or_nil(pos) - if not nod - or not nod.name - or not minetest.registered_nodes[nod.name] - or minetest.registered_nodes[nod.name].walkable == true then - return - end + nod = minetest.get_node_or_nil(pos) + if not nod + or not nod.name + or not minetest.registered_nodes[nod.name] + or minetest.registered_nodes[nod.name].walkable == true then + return + end - if minetest.setting_getbool("display_mob_spawn") then - minetest.chat_send_all("[mobs] Add "..name.." at "..minetest.pos_to_string(pos)) - end + if minetest.setting_getbool("display_mob_spawn") then + minetest.chat_send_all("[mobs] Add "..name.." at "..minetest.pos_to_string(pos)) + end - -- spawn mob half block higher - pos.y = pos.y - 0.5 - minetest.add_entity(pos, name) - --print ("Spawned "..name.." at "..minetest.pos_to_string(pos).." on "..node.name.." near "..neighbors[1]) + -- spawn mob half block higher + pos.y = pos.y - 0.5 + minetest.add_entity(pos, name) + --print ("Spawned "..name.." at "..minetest.pos_to_string(pos).." on "..node.name.." near "..neighbors[1]) - end - }) + end + }) end -- compatibility with older mob registration @@ -1353,7 +1361,7 @@ function effect(pos, amount, texture, max_size) minsize = 1, maxsize = (max_size or 2), texture = texture, - }) + }) end -- explosion @@ -1374,12 +1382,12 @@ function mobs:explosion(pos, radius, fire, smoke, sound) local c_brick = minetest.get_content_id("default:obsidianbrick") local c_chest = minetest.get_content_id("default:chest_locked") if sound - and sound ~= "" then + and sound ~= "" then minetest.sound_play(sound, { pos = pos, gain = 1.0, max_hear_distance = 16 - }) + }) end -- if area protected then no blast damage if minetest.is_protected(pos, "") then @@ -1394,7 +1402,7 @@ function mobs:explosion(pos, radius, fire, smoke, sound) p.z = pos.z + z if data[vi] ~= c_air and data[vi] ~= c_ignore and data[vi] ~= c_obsidian and data[vi] ~= c_brick - and data[vi] ~= c_chest then + and data[vi] ~= c_chest then local n = minetest.get_node(p).name if minetest.get_item_group(n, "unbreakable") ~= 1 then -- if chest then drop items inside @@ -1412,7 +1420,7 @@ function mobs:explosion(pos, radius, fire, smoke, sound) if fire > 0 and (minetest.registered_nodes[n].groups.flammable or math.random(1, 100) <= 30) then - -- minetest.set_node(p, {name = "fire:basic_flame"}) fire mod is disabled + -- minetest.set_node(p, {name = "fire:basic_flame"}) fire mod is disabled else minetest.remove_node(p) end @@ -1438,7 +1446,7 @@ function check_for_death(self) { object = self.object, max_hear_distance = self.sounds.distance - }) + }) end return false end @@ -1454,7 +1462,7 @@ function check_for_death(self) x = math.random(-1, 1), y = 5, z = math.random(-1, 1) - }) + }) end end end @@ -1464,7 +1472,7 @@ function check_for_death(self) { object = self.object, max_hear_distance = self.sounds.distance - }) + }) end if self.on_die then self.on_die(self, pos) @@ -1519,48 +1527,48 @@ function mobs:register_arrow(name, def) collisionbox = {0, 0, 0, 0, 0, 0}, -- remove box around arrows on_step = function(self, dtime) - self.timer = (self.timer or 0) + 1 - if self.timer > 150 then self.object:remove() return end + self.timer = (self.timer or 0) + 1 + if self.timer > 150 then self.object:remove() return end - local engage = 10 - (self.velocity / 2) -- clear entity before arrow becomes active - local pos = self.object:getpos() - local node = minetest.get_node_or_nil(self.object:getpos()) - if node then node = node.name else node = "air" end + local engage = 10 - (self.velocity / 2) -- clear entity before arrow becomes active + local pos = self.object:getpos() + local node = minetest.get_node_or_nil(self.object:getpos()) + if node then node = node.name else node = "air" end - if self.hit_node - and minetest.registered_nodes[node] - and minetest.registered_nodes[node].walkable then - self.hit_node(self, pos, node) - if self.drop == true then - pos.y = pos.y + 1 - self.lastpos = (self.lastpos or pos) - minetest.add_item(self.lastpos, self.object:get_luaentity().name) - end - self.object:remove() ; -- print ("hit node") - return - end + if self.hit_node + and minetest.registered_nodes[node] + and minetest.registered_nodes[node].walkable then + self.hit_node(self, pos, node) + if self.drop == true then + pos.y = pos.y + 1 + self.lastpos = (self.lastpos or pos) + minetest.add_item(self.lastpos, self.object:get_luaentity().name) + end + self.object:remove() ; -- print ("hit node") + return + end - if (self.hit_player or self.hit_mob) - and self.timer > engage then - for _,player in pairs(minetest.get_objects_inside_radius(pos, 1.0)) do - if self.hit_player - and player:is_player() then - self.hit_player(self, player) - self.object:remove() ; -- print ("hit player") - return - end - if self.hit_mob - and player:get_luaentity().name ~= self.object:get_luaentity().name - and player:get_luaentity().name ~= "__builtin:item" then - self.hit_mob(self, player) - self.object:remove() ; -- print ("hit mob") - return - end - end - end - self.lastpos = pos - end - }) + if (self.hit_player or self.hit_mob) + and self.timer > engage then + for _,player in pairs(minetest.get_objects_inside_radius(pos, 1.0)) do + if self.hit_player + and player:is_player() then + self.hit_player(self, player) + self.object:remove() ; -- print ("hit player") + return + end + if self.hit_mob + and player:get_luaentity().name ~= self.object:get_luaentity().name + and player:get_luaentity().name ~= "__builtin:item" then + self.hit_mob(self, player) + self.object:remove() ; -- print ("hit mob") + return + end + end + end + self.lastpos = pos + end + }) end -- Spawn Egg @@ -1572,22 +1580,22 @@ function mobs:register_egg(mob, desc, background) description = desc, inventory_image = invimg, on_place = function(itemstack, placer, pointed_thing) - local pos = pointed_thing.above - if pointed_thing.above - and not minetest.is_protected(pos, placer:get_player_name()) then - pos.y = pos.y + 0.5 - local mob = minetest.add_entity(pos, mob) - local ent = mob:get_luaentity() - if ent.type ~= "monster" then - -- set owner - ent.owner = placer:get_player_name() - ent.tamed = true - end - itemstack:take_item() - end - return itemstack - end, - }) + local pos = pointed_thing.above + if pointed_thing.above + and not minetest.is_protected(pos, placer:get_player_name()) then + pos.y = pos.y + 0.5 + local mob = minetest.add_entity(pos, mob) + local ent = mob:get_luaentity() + if ent.type ~= "monster" then + -- set owner + ent.owner = placer:get_player_name() + ent.tamed = true + end + itemstack:take_item() + end + return itemstack + end, + }) end -- capture critter (thanks to blert2112 for idea)