From cc77e109f52c22f77891f7f6cacbba77413c23aa Mon Sep 17 00:00:00 2001 From: cora Date: Thu, 10 Nov 2022 20:44:31 +0100 Subject: [PATCH] Split off general movement functions --- mods/ENTITIES/mcl_mobs/api.lua | 765 +--------------------------- mods/ENTITIES/mcl_mobs/combat.lua | 27 +- mods/ENTITIES/mcl_mobs/init.lua | 8 + mods/ENTITIES/mcl_mobs/movement.lua | 705 +++++++++++++++++++++++++ 4 files changed, 757 insertions(+), 748 deletions(-) diff --git a/mods/ENTITIES/mcl_mobs/api.lua b/mods/ENTITIES/mcl_mobs/api.lua index 22e75696f..702da72c9 100644 --- a/mods/ENTITIES/mcl_mobs/api.lua +++ b/mods/ENTITIES/mcl_mobs/api.lua @@ -138,46 +138,6 @@ function mob_class:object_in_range(object) return p1 and p2 and (vector.distance(p1, p2) <= dist) end --- Returns true is node can deal damage to self -local is_node_dangerous = function(self, nodename) - local nn = nodename - if self.lava_damage > 0 then - if minetest.get_item_group(nn, "lava") ~= 0 then - return true - end - end - if self.fire_damage > 0 then - if minetest.get_item_group(nn, "fire") ~= 0 then - return true - end - end - if minetest.registered_nodes[nn] and minetest.registered_nodes[nn].damage_per_second and minetest.registered_nodes[nn].damage_per_second > 0 then - return true - end - return false -end - - --- Returns true if node is a water hazard -local is_node_waterhazard = function(self, nodename) - local nn = nodename - if self.water_damage > 0 then - if minetest.get_item_group(nn, "water") ~= 0 then - return true - end - end - if minetest.registered_nodes[nn] and minetest.registered_nodes[nn].drowning and minetest.registered_nodes[nn].drowning > 0 then - if self.breath_max ~= -1 then - -- check if the mob is water-breathing _and_ the block is water; only return true if neither is the case - -- this will prevent water-breathing mobs to classify water or e.g. sand below them as dangerous - if not self.breathes_in_water and minetest.get_item_group(nn, "water") ~= 0 then - return true - end - end - end - return false -end - -- check if within physical map limits (-30911 to 30927) local function within_limits(pos, radius) local wmin, wmax = -30912, 30928 @@ -210,670 +170,6 @@ local node_ok = function(pos, fallback) return minetest.registered_nodes[fallback] end - -local can_jump_cliff = function(self) - local yaw = self.object:get_yaw() - local pos = self.object:get_pos() - local v = self.object:get_velocity() - - local v2 = math.abs(v.x)+math.abs(v.z)*.833 - local jump_c_multiplier = 1 - if v2/self.walk_velocity/2>1 then - jump_c_multiplier = v2/self.walk_velocity/2 - end - - -- where is front - local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 - local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 - - --is there nothing under the block in front? if so jump the gap. - local nodLow = node_ok({ - x = pos.x + dir_x-0.6, - y = pos.y - 0.5, - z = pos.z + dir_z-0.6 - }, "air") - - local nodFar = node_ok({ - x = pos.x + dir_x*2, - y = pos.y - 0.5, - z = pos.z + dir_z*2 - }, "air") - - local nodFar2 = node_ok({ - x = pos.x + dir_x*2.5, - y = pos.y - 0.5, - z = pos.z + dir_z*2.5 - }, "air") - - - if minetest.registered_nodes[nodLow.name] - and minetest.registered_nodes[nodLow.name].walkable ~= true - - - and (minetest.registered_nodes[nodFar.name] - and minetest.registered_nodes[nodFar.name].walkable == true - - or minetest.registered_nodes[nodFar2.name] - and minetest.registered_nodes[nodFar2.name].walkable == true) - - then - --disable fear heigh while we make our jump - self._jumping_cliff = true - minetest.after(1, function() - if self and self.object then - self._jumping_cliff = false - end - end) - return true - else - return false - end -end - --- is mob facing a cliff or danger -local is_at_cliff_or_danger = function(self) - - if self.fear_height == 0 or can_jump_cliff(self) or self._jumping_cliff or not self.object:get_luaentity() then -- 0 for no falling protection! - return false - end - - local yaw = self.object:get_yaw() - 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:get_pos() - local ypos = pos.y + self.collisionbox[2] -- just above floor - - local free_fall, blocker = 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}) - if free_fall then - return true - else - local bnode = minetest.get_node(blocker) - local danger = is_node_dangerous(self, bnode.name) - if danger then - return true - else - local def = minetest.registered_nodes[bnode.name] - if def and def.walkable then - return false - end - end - end - - return false -end - - --- copy the 'mob facing cliff_or_danger check' from above, and rework to avoid water -local is_at_water_danger = function(self) - - - if not self.object:get_luaentity() or can_jump_cliff(self) or self._jumping_cliff then - return false - end - local yaw = self.object:get_yaw() - 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:get_pos() - local ypos = pos.y + self.collisionbox[2] -- just above floor - - local free_fall, blocker = minetest.line_of_sight( - {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, - {x = pos.x + dir_x, y = ypos - 3, z = pos.z + dir_z}) - if free_fall then - return true - else - local bnode = minetest.get_node(blocker) - local waterdanger = is_node_waterhazard(self, bnode.name) - if - waterdanger and (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) then - return false - elseif waterdanger and (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) == false then - return true - else - local def = minetest.registered_nodes[bnode.name] - if def and def.walkable then - return false - end - end - end - - return false -end - --- jump if facing a solid node (not fences or gates) -local do_jump = function(self) - if not self.jump - or self.jump_height == 0 - or self.fly - or (self.child and self.type ~= "monster") - or self.order == "stand" then - return false - end - - self.facing_fence = false - - -- something stopping us while moving? - if self.state ~= "stand" - and self:get_velocity() > 0.5 - and self.object:get_velocity().y ~= 0 then - return false - end - - local pos = self.object:get_pos() - local yaw = self.object:get_yaw() - - -- what is mob standing on? - pos.y = pos.y + self.collisionbox[2] - 0.2 - - local nod = node_ok(pos) - - if minetest.registered_nodes[nod.name].walkable == false then - return false - end - - local v = self.object:get_velocity() - local v2 = math.abs(v.x)+math.abs(v.z)*.833 - local jump_c_multiplier = 1 - if v2/self.walk_velocity/2>1 then - jump_c_multiplier = v2/self.walk_velocity/2 - end - - -- where is front - local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 - local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 - - -- what is in front of mob? - nod = node_ok({ - x = pos.x + dir_x, - y = pos.y + 0.5, - z = pos.z + dir_z - }) - - -- this is used to detect if there's a block on top of the block in front of the mob. - -- If there is, there is no point in jumping as we won't manage. - local nodTop = node_ok({ - x = pos.x + dir_x, - y = pos.y + 1.5, - z = pos.z + dir_z - }, "air") - - - -- we don't attempt to jump if there's a stack of blocks blocking - if minetest.registered_nodes[nodTop.name].walkable == true and not (self.attack and self.state == "attack") then - return false - end - - -- thin blocks that do not need to be jumped - if nod.name == node_snow then - return false - end - - local ndef = minetest.registered_nodes[nod.name] - if self.walk_chance == 0 or ndef and ndef.walkable or can_jump_cliff(self) then - - if minetest.get_item_group(nod.name, "fence") == 0 - and minetest.get_item_group(nod.name, "fence_gate") == 0 - and minetest.get_item_group(nod.name, "wall") == 0 then - - local v = self.object:get_velocity() - - v.y = self.jump_height + 0.1 * 3 - - if can_jump_cliff(self) then - v=vector.multiply(v, vector.new(2.8,1,2.8)) - end - - self:set_animation( "jump") -- only when defined - - self.object:set_velocity(v) - - -- when in air move forward - minetest.after(0.3, function(self, v) - if (not self.object) or (not self.object:get_luaentity()) or (self.state == "die") then - return - end - self.object:set_acceleration({ - x = v.x * 2, - y = DEFAULT_FALL_SPEED, - z = v.z * 2, - }) - end, self, v) - - if self.jump_sound_cooloff <= 0 then - self:mob_sound("jump") - self.jump_sound_cooloff = 0.5 - end - else - self.facing_fence = true - end - - -- if we jumped against a block/wall 4 times then turn - if self.object:get_velocity().x ~= 0 - and self.object:get_velocity().z ~= 0 then - - self.jump_count = (self.jump_count or 0) + 1 - - if self.jump_count == 4 then - - local yaw = self.object:get_yaw() or 0 - - yaw = self:set_yaw( yaw + 1.35, 8) - - self.jump_count = 0 - end - end - - return true - end - - return false -end - --- should mob follow what I'm holding ? -local follow_holding = function(self, clicker) - if self.nofollow then return false end - - if mcl_mobs.invis[clicker:get_player_name()] then - return false - end - - 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 - - --- find and replace what mob is looking for (grass, wheat etc.) -local replace = function(self, pos) - - if not self.replace_rate - or not self.replace_what - or self.child == true - or self.object:get_velocity().y ~= 0 - or math.random(1, self.replace_rate) > 1 then - return - end - - local what, with, y_offset - - if type(self.replace_what[1]) == "table" then - - local num = math.random(#self.replace_what) - - what = self.replace_what[num][1] or "" - with = self.replace_what[num][2] or "" - y_offset = self.replace_what[num][3] or 0 - else - what = self.replace_what - with = self.replace_with or "" - y_offset = self.replace_offset or 0 - end - - pos.y = pos.y + y_offset - - local node = minetest.get_node(pos) - if node.name == what then - - local oldnode = {name = what, param2 = node.param2} - local newnode = {name = with, param2 = node.param2} - local on_replace_return - - if self.on_replace then - on_replace_return = self.on_replace(self, pos, oldnode, newnode) - end - - if on_replace_return ~= false then - - if mobs_griefing then - minetest.set_node(pos, newnode) - end - - end - end -end - - --- check if daytime and also if mob is docile during daylight hours -local day_docile = function(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 - - - --- specific runaway -local specific_runaway = function(list, what) - - -- no list so do not run - if list == nil then - return false - end - - -- found entity on list to attack? - for no = 1, #list do - - if list[no] == what then - return true - end - end - - return false -end - - --- find someone to runaway from -local runaway_from = function(self) - - if not self.runaway_from and self.state ~= "flop" then - return - end - - local s = self.object:get_pos() - local p, sp, dist - local player, obj, min_player - local type, name = "", "" - local min_dist = self.view_range + 1 - local objs = minetest.get_objects_inside_radius(s, self.view_range) - - for n = 1, #objs do - - if objs[n]:is_player() then - - if mcl_mobs.invis[ objs[n]:get_player_name() ] - or self.owner == objs[n]:get_player_name() - or (not self:object_in_range(objs[n])) then - type = "" - else - player = objs[n] - type = "player" - name = "player" - end - else - obj = objs[n]:get_luaentity() - - if obj then - player = obj.object - type = obj.type - name = obj.name or "" - end - end - - -- find specific mob to runaway from - if name ~= "" and name ~= self.name - and specific_runaway(self.runaway_from, name) then - - p = player:get_pos() - sp = s - - -- aim higher to make looking up hills more realistic - p.y = p.y + 1 - sp.y = sp.y + 1 - - dist = vector.distance(p, s) - - - -- choose closest player/mpb to runaway from - if dist < min_dist - and self:line_of_sight(sp, p, 2) == true then - min_dist = dist - min_player = player - end - end - end - - if min_player then - - local lp = player:get_pos() - local vec = { - x = lp.x - s.x, - y = lp.y - s.y, - z = lp.z - s.z - } - - local yaw = (atan(vec.z / vec.x) + 3 *math.pi/ 2) - self.rotate - - if lp.x > s.x then - yaw = yaw + pi - end - - yaw = self:set_yaw( yaw, 4) - self.state = "runaway" - self.runaway_timer = 3 - self.following = nil - end -end - - --- follow player if owner or holding item, if fish outta water then flop -local follow_flop = function(self) - - -- find player to follow - if (self.follow ~= "" - or self.order == "follow") - and not self.following - and self.state ~= "attack" - and self.order ~= "sit" - and self.state ~= "runaway" then - - local s = self.object:get_pos() - local players = minetest.get_connected_players() - - for n = 1, #players do - - if (self:object_in_range(players[n])) - and not mcl_mobs.invis[ players[n]:get_player_name() ] then - - self.following = players[n] - - break - end - end - end - - if self.type == "npc" - and self.order == "follow" - and self.state ~= "attack" - and self.order ~= "sit" - 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, - -- mob is horny, fleeing or attacking - if self.following - and self.following:is_player() - and (follow_holding(self, self.following) == false or - self.horny or self.state == "runaway") then - self.following = nil - end - - end - - -- follow that thing - if self.following then - - local s = self.object:get_pos() - local p - - if self.following:is_player() then - - p = self.following:get_pos() - - elseif self.following.object then - - p = self.following.object:get_pos() - end - - if p then - - local dist = vector.distance(p, s) - - -- dont follow if out of range - if (not self:object_in_range(self.following)) then - self.following = nil - else - local vec = { - x = p.x - s.x, - z = p.z - s.z - } - - local yaw = (atan(vec.z / vec.x) +math.pi/ 2) - self.rotate - - if p.x > s.x then yaw = yaw +math.pi end - - self:set_yaw( yaw, 2.35) - - -- anyone but standing npc's can move along - if dist > 3 - and self.order ~= "stand" then - - self:set_velocity(self.follow_velocity) - - if self.walk_chance ~= 0 then - self:set_animation( "run") - end - else - self:set_velocity(0) - self:set_animation( "stand") - end - - return - end - end - end - - -- swimmers flop when out of their element, and swim again when back in - if self.fly then - local s = self.object:get_pos() - if self:flight_check( s) == false then - - self.state = "flop" - self.object:set_acceleration({x = 0, y = DEFAULT_FALL_SPEED, z = 0}) - - local p = self.object:get_pos() - local sdef = minetest.registered_nodes[node_ok(vector.add(p, vector.new(0,self.collisionbox[2]-0.2,0))).name] - -- Flop on ground - if sdef and sdef.walkable then - if self.object:get_velocity().y < 0.1 then - self:mob_sound("flop") - self.object:set_velocity({ - x = math.random(-FLOP_HOR_SPEED, FLOP_HOR_SPEED), - y = FLOP_HEIGHT, - z = math.random(-FLOP_HOR_SPEED, FLOP_HOR_SPEED), - }) - end - end - - self:set_animation( "stand", true) - - return - elseif self.state == "flop" then - self.state = "stand" - self.object:set_acceleration({x = 0, y = 0, z = 0}) - self:set_velocity(0) - end - end -end - -local function go_to_pos(entity,b) - if not entity then return end - local s=entity.object:get_pos() - if not b then - --self.state = "stand" - return end - if vector.distance(b,s) < 1 then - --entity:set_velocity(0) - return true - end - local v = { x = b.x - s.x, z = b.z - s.z } - local yaw = (atann(v.z / v.x) +math.pi/ 2) - entity.rotate - if b.x > s.x then yaw = yaw +math.pi end - entity.object:set_yaw(yaw) - entity:set_velocity(entity.follow_velocity) - entity:set_animation("walk") -end - -local function interact_with_door(self, action, target) - local p = self.object:get_pos() - --local t = minetest.get_timeofday() - --local dd = minetest.find_nodes_in_area(vector.offset(p,-1,-1,-1),vector.offset(p,1,1,1),{"group:door"}) - --for _,d in pairs(dd) do - if target then - mcl_log("Door target is: ".. minetest.pos_to_string(target)) - - local n = minetest.get_node(target) - if n.name:find("_b_") or n.name:find("_t_") then - mcl_log("Door") - local def = minetest.registered_nodes[n.name] - local closed = n.name:find("_b_1") or n.name:find("_t_1") - --if self.state == PATHFINDING then - if closed and action == "open" and def.on_rightclick then - mcl_log("Open door") - def.on_rightclick(target,n,self) - end - if not closed and action == "close" and def.on_rightclick then - mcl_log("Close door") - def.on_rightclick(target,n,self) - end - --else - else - mcl_log("Not door") - end - else - mcl_log("no target. cannot try and open or close door") - end - --end -end - -local function do_pathfind_action (self, action) - if action then - mcl_log("Action present") - local type = action["type"] - local action_val = action["action"] - local target = action["target"] - if target then - mcl_log("Target: ".. minetest.pos_to_string(target)) - end - if type and type == "door" then - mcl_log("Type is door") - interact_with_door(self, action_val, target) - end - end -end - -- execute current state (stand, walk, run, attacks) -- returns true if mob has died local do_states = function(self, dtime) @@ -926,7 +222,7 @@ local do_states = function(self, dtime) if self.walk_chance ~= 0 and self.facing_fence ~= true and math.random(1, 100) <= self.walk_chance - and is_at_cliff_or_danger(self) == false then + and self:is_at_cliff_or_danger() == false then self:set_velocity(self.walk_velocity) self.state = "walk" @@ -965,8 +261,8 @@ local do_states = function(self, dtime) local is_in_danger = false if lp then -- If mob in or on dangerous block, look for land - if (is_node_dangerous(self, self.standing_in) or - is_node_dangerous(self, self.standing_on)) or (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) and (not self.fly) then + if (self:is_node_dangerous(self.standing_in) or + self:is_node_dangerous(self.standing_on)) or (self:is_node_waterhazard(self.standing_in) or self:is_node_waterhazard(self.standing_on)) and (not self.fly) then is_in_danger = true -- If mob in or on dangerous block, look for land @@ -1020,7 +316,7 @@ local do_states = function(self, dtime) -- stand for great fall or danger or fence in front local cliff_or_danger = false if is_in_danger then - cliff_or_danger = is_at_cliff_or_danger(self) + cliff_or_danger = self:is_at_cliff_or_danger() end if self.facing_fence == true or cliff_or_danger @@ -1052,7 +348,7 @@ local do_states = function(self, dtime) -- stop after 5 seconds or when at cliff if self.runaway_timer > 5 - or is_at_cliff_or_danger(self) then + or self:is_at_cliff_or_danger() then self.runaway_timer = 0 self:set_velocity(0) self.state = "stand" @@ -1285,7 +581,7 @@ local do_states = function(self, dtime) self:smart_mobs(s, p, dist, dtime) end - if is_at_cliff_or_danger(self) then + if self:is_at_cliff_or_danger() then self:set_velocity( 0) self:set_animation( "stand") @@ -1486,15 +782,6 @@ local function check_herd(self,dtime) end end -local teleport = function(self, target) - if self.do_teleport then - if self.do_teleport(self, target) == false then - return - end - end -end - - -- deal damage and effects when mob punched local mob_punch = function(self, hitter, tflp, tool_capabilities, dir) @@ -2014,24 +1301,10 @@ local mob_activate = function(self, staticdata, def, dtime) -- run after_activate if def.after_activate then - def.after_activate(self, staticdata, def, dtime) end end -local function check_aggro(self,dtime) - if not self._aggro or not self.attack then return end - if not self._check_aggro_timer or self._check_aggro_timer > 5 then - self._check_aggro_timer = 0 - if not self.attack:get_pos() or vector.distance(self.attack:get_pos(),self.object:get_pos()) > 128 then - self._aggro = nil - self.attack = nil - self.state = "stand" - end - end - self._check_aggro_timer = self._check_aggro_timer + dtime -end - -- main mob function local mob_step = function(self, dtime) self.lifetimer = self.lifetimer - dtime @@ -2088,7 +1361,7 @@ local mob_step = function(self, dtime) self.object:set_velocity({x = v.x*d, y = v.y, z = v.z*d}) end - check_aggro(self,dtime) + self:check_aggro(dtime) self:check_item_pickup() self:check_particlespawners(dtime) @@ -2120,7 +1393,7 @@ local mob_step = function(self, dtime) end --Mob following code. - follow_flop(self) + self:follow_flop() --set animation speed relitive to velocity local v = self.object:get_velocity() @@ -2314,13 +1587,11 @@ local mob_step = function(self, dtime) end -- node replace check (cow eats grass etc.) - replace(self, pos) + self:replace(pos) end self:monster_attack() - self:npc_attack() - self:check_breeding() if do_states(self, dtime) then @@ -2331,13 +1602,13 @@ local mob_step = function(self, dtime) return false end - do_jump(self) + self:do_jump() self:set_armor_texture() - runaway_from(self) + self:check_runaway_from() - if is_at_water_danger(self) and self.state ~= "attack" then + if self:is_at_water_danger() and self.state ~= "attack" then if math.random(1, 10) <= 6 then self:set_velocity(0) self.state = "stand" @@ -2389,7 +1660,7 @@ local mob_step = function(self, dtime) return end - if is_at_cliff_or_danger(self) then + if self:is_at_cliff_or_danger() then self:set_velocity(0) self.state = "stand" self:set_animation( "stand") @@ -2400,9 +1671,8 @@ end -- default function when mobs are blown up with TNT -local do_tnt = function(obj, damage) - - obj.object:punch(obj.object, 1.0, { +local function do_tnt(damage) + self.object:punch(self.object, 1.0, { full_punch_interval = 1.0, damage_groups = {fleshy = damage}, }, nil) @@ -2449,6 +1719,9 @@ local create_mob_on_rightclick = function(on_rightclick) end end + + +--#### REGISTER FUNCS -- register mob entity function mcl_mobs.register_mob(name, def) @@ -2480,7 +1753,6 @@ if collisionbox[5] < 0.79 then end minetest.register_entity(name, setmetatable({ - use_texture_alpha = def.use_texture_alpha, head_swivel = def.head_swivel or nil, -- bool to activate this function head_yaw_offset = def.head_yaw_offset or 0, -- for wonkey model bones @@ -2604,7 +1876,6 @@ minetest.register_entity(name, setmetatable({ shooter_avoid_enemy = def.shooter_avoid_enemy, strafes = def.strafes, avoid_distance = def.avoid_distance or 9, - teleport = teleport, do_teleport = def.do_teleport, spawn_class = def.spawn_class, can_spawn = def.can_spawn, diff --git a/mods/ENTITIES/mcl_mobs/combat.lua b/mods/ENTITIES/mcl_mobs/combat.lua index b44fc0ac1..8cd425821 100644 --- a/mods/ENTITIES/mcl_mobs/combat.lua +++ b/mods/ENTITIES/mcl_mobs/combat.lua @@ -6,6 +6,17 @@ local enable_pathfinding = true local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching local stuck_path_timeout = 10 -- how long will mob follow path before giving up +-- check if daytime and also if mob is docile during daylight hours +function mob_class:day_docile() + if self.docile_by_day == false then + return false + elseif self.docile_by_day == true + and self.time_of_day > 0.2 + and self.time_of_day < 0.8 then + return true + end +end + -- attack player/mob function mob_class:do_attack(player) @@ -307,7 +318,7 @@ function mob_class:monster_attack() if not damage_enabled or self.passive ~= false or self.state == "attack" - or day_docile(self) then + or self:day_docile() then return end @@ -490,3 +501,17 @@ function mob_class:boom(pos, strength, fire) -- delete the object after it punched the player to avoid nil entities in e.g. mcl_shields!! self.object:remove() end + + +function mob_class:check_aggro(dtime) + if not self._aggro or not self.attack then return end + if not self._check_aggro_timer or self._check_aggro_timer > 5 then + self._check_aggro_timer = 0 + if not self.attack:get_pos() or vector.distance(self.attack:get_pos(),self.object:get_pos()) > 128 then + self._aggro = nil + self.attack = nil + self.state = "stand" + end + end + self._check_aggro_timer = self._check_aggro_timer + dtime +end diff --git a/mods/ENTITIES/mcl_mobs/init.lua b/mods/ENTITIES/mcl_mobs/init.lua index 8be12d8f7..20f215d4b 100644 --- a/mods/ENTITIES/mcl_mobs/init.lua +++ b/mods/ENTITIES/mcl_mobs/init.lua @@ -5,14 +5,22 @@ mcl_mobs.mob_class_meta = {__index = mcl_mobs.mob_class} local path = minetest.get_modpath(minetest.get_current_modname()) --api and helpers +-- effects: sounds and particles mostly dofile(path .. "/effects.lua") +-- physics: involuntary mob movement - particularly falling and death dofile(path .. "/physics.lua") +-- movement: general voluntary mob movement, walking avoiding cliffs etc. dofile(path .. "/movement.lua") +-- items: item management for mobs dofile(path .. "/items.lua") +-- pathfinding: pathfinding to target positions dofile(path .. "/pathfinding.lua") +-- combat: attack logic dofile(path .. "/combat.lua") +-- the enity functions themselves dofile(path .. "/api.lua") + --utility functions dofile(path .. "/breeding.lua") dofile(path .. "/spawning.lua") diff --git a/mods/ENTITIES/mcl_mobs/movement.lua b/mods/ENTITIES/mcl_mobs/movement.lua index 20dff2d62..1c5667625 100644 --- a/mods/ENTITIES/mcl_mobs/movement.lua +++ b/mods/ENTITIES/mcl_mobs/movement.lua @@ -1,5 +1,58 @@ local math, vector, minetest, mcl_mobs = math, vector, minetest, mcl_mobs local mob_class = mcl_mobs.mob_class +local DEFAULT_FALL_SPEED = -9.81*1.5 +local FLOP_HEIGHT = 6 +local FLOP_HOR_SPEED = 1.5 + +-- get node but use fallback for nil or unknown +local node_ok = function(pos, fallback) + fallback = fallback or mcl_mobs.fallback_node + local node = minetest.get_node_or_nil(pos) + if node and minetest.registered_nodes[node.name] then + return node + end + return minetest.registered_nodes[fallback] +end + +-- Returns true is node can deal damage to self +function mob_class:is_node_dangerous(nodename) + local nn = nodename + if self.lava_damage > 0 then + if minetest.get_item_group(nn, "lava") ~= 0 then + return true + end + end + if self.fire_damage > 0 then + if minetest.get_item_group(nn, "fire") ~= 0 then + return true + end + end + if minetest.registered_nodes[nn] and minetest.registered_nodes[nn].damage_per_second and minetest.registered_nodes[nn].damage_per_second > 0 then + return true + end + return false +end + + +-- Returns true if node is a water hazard +function mob_class:is_node_waterhazard(nodename) + local nn = nodename + if self.water_damage > 0 then + if minetest.get_item_group(nn, "water") ~= 0 then + return true + end + end + if minetest.registered_nodes[nn] and minetest.registered_nodes[nn].drowning and minetest.registered_nodes[nn].drowning > 0 then + if self.breath_max ~= -1 then + -- check if the mob is water-breathing _and_ the block is water; only return true if neither is the case + -- this will prevent water-breathing mobs to classify water or e.g. sand below them as dangerous + if not self.breathes_in_water and minetest.get_item_group(nn, "water") ~= 0 then + return true + end + end + end + return false +end -- check line of sight (BrunoMine) function mob_class:line_of_sight(pos1, pos2, stepsize) @@ -69,3 +122,655 @@ function mob_class:line_of_sight(pos1, pos2, stepsize) return false end + +function mob_class:can_jump_cliff() + local yaw = self.object:get_yaw() + local pos = self.object:get_pos() + local v = self.object:get_velocity() + + local v2 = math.abs(v.x)+math.abs(v.z)*.833 + local jump_c_multiplier = 1 + if v2/self.walk_velocity/2>1 then + jump_c_multiplier = v2/self.walk_velocity/2 + end + + -- where is front + local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 + local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 + + --is there nothing under the block in front? if so jump the gap. + local nodLow = node_ok({ + x = pos.x + dir_x-0.6, + y = pos.y - 0.5, + z = pos.z + dir_z-0.6 + }, "air") + + local nodFar = node_ok({ + x = pos.x + dir_x*2, + y = pos.y - 0.5, + z = pos.z + dir_z*2 + }, "air") + + local nodFar2 = node_ok({ + x = pos.x + dir_x*2.5, + y = pos.y - 0.5, + z = pos.z + dir_z*2.5 + }, "air") + + + if minetest.registered_nodes[nodLow.name] + and minetest.registered_nodes[nodLow.name].walkable ~= true + + + and (minetest.registered_nodes[nodFar.name] + and minetest.registered_nodes[nodFar.name].walkable == true + + or minetest.registered_nodes[nodFar2.name] + and minetest.registered_nodes[nodFar2.name].walkable == true) + + then + --disable fear heigh while we make our jump + self._jumping_cliff = true + minetest.after(1, function() + if self and self.object then + self._jumping_cliff = false + end + end) + return true + else + return false + end +end + +-- is mob facing a cliff or danger +function mob_class:is_at_cliff_or_danger() + if self.fear_height == 0 or self:can_jump_cliff() or self._jumping_cliff or not self.object:get_luaentity() then -- 0 for no falling protection! + return false + end + + local yaw = self.object:get_yaw() + 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:get_pos() + local ypos = pos.y + self.collisionbox[2] -- just above floor + + local free_fall, blocker = 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}) + if free_fall then + return true + else + local bnode = minetest.get_node(blocker) + local danger = self:is_node_dangerous(bnode.name) + if danger then + return true + else + local def = minetest.registered_nodes[bnode.name] + if def and def.walkable then + return false + end + end + end + + return false +end + + +-- copy the 'mob facing cliff_or_danger check' from above, and rework to avoid water +function mob_class:is_at_water_danger() + if not self.object:get_luaentity() or self:can_jump_cliff() or self._jumping_cliff then + return false + end + local yaw = self.object:get_yaw() + 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:get_pos() + local ypos = pos.y + self.collisionbox[2] -- just above floor + + local free_fall, blocker = minetest.line_of_sight( + {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, + {x = pos.x + dir_x, y = ypos - 3, z = pos.z + dir_z}) + if free_fall then + return true + else + local bnode = minetest.get_node(blocker) + local waterdanger = self:is_node_waterhazard(bnode.name) + if + waterdanger and (self:is_node_waterhazard(self.standing_in) or self:is_node_waterhazard( self.standing_on)) then + return false + elseif waterdanger and (self:is_node_waterhazard(self.standing_in) or self:is_node_waterhazard(self.standing_on)) == false then + return true + else + local def = minetest.registered_nodes[bnode.name] + if def and def.walkable then + return false + end + end + end + + return false +end + +-- jump if facing a solid node (not fences or gates) +function mob_class:do_jump() + if not self.jump + or self.jump_height == 0 + or self.fly + or (self.child and self.type ~= "monster") + or self.order == "stand" then + return false + end + + self.facing_fence = false + + -- something stopping us while moving? + if self.state ~= "stand" + and self:get_velocity() > 0.5 + and self.object:get_velocity().y ~= 0 then + return false + end + + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + + -- what is mob standing on? + pos.y = pos.y + self.collisionbox[2] - 0.2 + + local nod = node_ok(pos) + + if minetest.registered_nodes[nod.name].walkable == false then + return false + end + + local v = self.object:get_velocity() + local v2 = math.abs(v.x)+math.abs(v.z)*.833 + local jump_c_multiplier = 1 + if v2/self.walk_velocity/2>1 then + jump_c_multiplier = v2/self.walk_velocity/2 + end + + -- where is front + local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 + local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5)*jump_c_multiplier+0.6 + + -- what is in front of mob? + nod = node_ok({ + x = pos.x + dir_x, + y = pos.y + 0.5, + z = pos.z + dir_z + }) + + -- this is used to detect if there's a block on top of the block in front of the mob. + -- If there is, there is no point in jumping as we won't manage. + local nodTop = node_ok({ + x = pos.x + dir_x, + y = pos.y + 1.5, + z = pos.z + dir_z + }, "air") + + + -- we don't attempt to jump if there's a stack of blocks blocking + if minetest.registered_nodes[nodTop.name].walkable == true and not (self.attack and self.state == "attack") then + return false + end + + -- thin blocks that do not need to be jumped + if nod.name == node_snow then + return false + end + + local ndef = minetest.registered_nodes[nod.name] + if self.walk_chance == 0 or ndef and ndef.walkable or self:can_jump_cliff() then + + if minetest.get_item_group(nod.name, "fence") == 0 + and minetest.get_item_group(nod.name, "fence_gate") == 0 + and minetest.get_item_group(nod.name, "wall") == 0 then + + local v = self.object:get_velocity() + + v.y = self.jump_height + 0.1 * 3 + + if self:can_jump_cliff() then + v=vector.multiply(v, vector.new(2.8,1,2.8)) + end + + self:set_animation( "jump") -- only when defined + + self.object:set_velocity(v) + + -- when in air move forward + minetest.after(0.3, function(self, v) + if (not self.object) or (not self.object:get_luaentity()) or (self.state == "die") then + return + end + self.object:set_acceleration({ + x = v.x * 2, + y = DEFAULT_FALL_SPEED, + z = v.z * 2, + }) + end, self, v) + + if self.jump_sound_cooloff <= 0 then + self:mob_sound("jump") + self.jump_sound_cooloff = 0.5 + end + else + self.facing_fence = true + end + + -- if we jumped against a block/wall 4 times then turn + if self.object:get_velocity().x ~= 0 + and self.object:get_velocity().z ~= 0 then + + self.jump_count = (self.jump_count or 0) + 1 + + if self.jump_count == 4 then + + local yaw = self.object:get_yaw() or 0 + + yaw = self:set_yaw( yaw + 1.35, 8) + + self.jump_count = 0 + end + end + + return true + end + + return false +end + +-- should mob follow what I'm holding ? +function mob_class:follow_holding(clicker) + if self.nofollow then return false end + + if mcl_mobs.invis[clicker:get_player_name()] then + return false + end + + 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 + + +-- find and replace what mob is looking for (grass, wheat etc.) +function mob_class:replace(pos) + + if not self.replace_rate + or not self.replace_what + or self.child == true + or self.object:get_velocity().y ~= 0 + or math.random(1, self.replace_rate) > 1 then + return + end + + local what, with, y_offset + + if type(self.replace_what[1]) == "table" then + + local num = math.random(#self.replace_what) + + what = self.replace_what[num][1] or "" + with = self.replace_what[num][2] or "" + y_offset = self.replace_what[num][3] or 0 + else + what = self.replace_what + with = self.replace_with or "" + y_offset = self.replace_offset or 0 + end + + pos.y = pos.y + y_offset + + local node = minetest.get_node(pos) + if node.name == what then + + local oldnode = {name = what, param2 = node.param2} + local newnode = {name = with, param2 = node.param2} + local on_replace_return + + if self.on_replace then + on_replace_return = self.on_replace(self, pos, oldnode, newnode) + end + + if on_replace_return ~= false then + + if mobs_griefing then + minetest.set_node(pos, newnode) + end + + end + end +end + +-- specific runaway +local specific_runaway = function(list, what) + if type(list) ~= "table" then + list = {} + end + + -- no list so do not run + if list == nil then + return false + end + + -- found entity on list to attack? + for no = 1, #list do + + if list[no] == what then + return true + end + end + + return false +end + + +-- find someone to runaway from +function mob_class:check_runaway_from() + if not self.runaway_from and self.state ~= "flop" then + return + end + + local s = self.object:get_pos() + local p, sp, dist + local player, obj, min_player + local type, name = "", "" + local min_dist = self.view_range + 1 + local objs = minetest.get_objects_inside_radius(s, self.view_range) + + for n = 1, #objs do + + if objs[n]:is_player() then + + if mcl_mobs.invis[ objs[n]:get_player_name() ] + or self.owner == objs[n]:get_player_name() + or (not self:object_in_range(objs[n])) then + type = "" + else + player = objs[n] + type = "player" + name = "player" + end + else + obj = objs[n]:get_luaentity() + + if obj then + player = obj.object + type = obj.type + name = obj.name or "" + end + end + + -- find specific mob to runaway from + if name ~= "" and name ~= self.name + and specific_runaway(self.runaway_from, name) then + + p = player:get_pos() + sp = s + + -- aim higher to make looking up hills more realistic + p.y = p.y + 1 + sp.y = sp.y + 1 + + dist = vector.distance(p, s) + + + -- choose closest player/mpb to runaway from + if dist < min_dist + and self:line_of_sight(sp, p, 2) == true then + min_dist = dist + min_player = player + end + end + end + + if min_player then + + local lp = player:get_pos() + local vec = { + x = lp.x - s.x, + y = lp.y - s.y, + z = lp.z - s.z + } + + local yaw = (atan(vec.z / vec.x) + 3 *math.pi/ 2) - self.rotate + + if lp.x > s.x then + yaw = yaw + pi + end + + yaw = self:set_yaw( yaw, 4) + self.state = "runaway" + self.runaway_timer = 3 + self.following = nil + end +end + + +-- follow player if owner or holding item, if fish outta water then flop +function mob_class:follow_flop() + + -- find player to follow + if (self.follow ~= "" + or self.order == "follow") + and not self.following + and self.state ~= "attack" + and self.order ~= "sit" + and self.state ~= "runaway" then + + local s = self.object:get_pos() + local players = minetest.get_connected_players() + + for n = 1, #players do + + if (self:object_in_range(players[n])) + and not mcl_mobs.invis[ players[n]:get_player_name() ] then + + self.following = players[n] + + break + end + end + end + + if self.type == "npc" + and self.order == "follow" + and self.state ~= "attack" + and self.order ~= "sit" + 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, + -- mob is horny, fleeing or attacking + if self.following + and self.following:is_player() + and (self:follow_holding(self.following) == false or + self.horny or self.state == "runaway") then + self.following = nil + end + + end + + -- follow that thing + if self.following then + + local s = self.object:get_pos() + local p + + if self.following:is_player() then + + p = self.following:get_pos() + + elseif self.following.object then + + p = self.following.object:get_pos() + end + + if p then + + local dist = vector.distance(p, s) + + -- dont follow if out of range + if (not self:object_in_range(self.following)) then + self.following = nil + else + local vec = { + x = p.x - s.x, + z = p.z - s.z + } + + local yaw = (atan(vec.z / vec.x) +math.pi/ 2) - self.rotate + + if p.x > s.x then yaw = yaw +math.pi end + + self:set_yaw( yaw, 2.35) + + -- anyone but standing npc's can move along + if dist > 3 + and self.order ~= "stand" then + + self:set_velocity(self.follow_velocity) + + if self.walk_chance ~= 0 then + self:set_animation( "run") + end + else + self:set_velocity(0) + self:set_animation( "stand") + end + + return + end + end + end + + -- swimmers flop when out of their element, and swim again when back in + if self.fly then + local s = self.object:get_pos() + if self:flight_check( s) == false then + + self.state = "flop" + self.object:set_acceleration({x = 0, y = DEFAULT_FALL_SPEED, z = 0}) + + local p = self.object:get_pos() + local sdef = minetest.registered_nodes[node_ok(vector.add(p, vector.new(0,self.collisionbox[2]-0.2,0))).name] + -- Flop on ground + if sdef and sdef.walkable then + if self.object:get_velocity().y < 0.1 then + self:mob_sound("flop") + self.object:set_velocity({ + x = math.random(-FLOP_HOR_SPEED, FLOP_HOR_SPEED), + y = FLOP_HEIGHT, + z = math.random(-FLOP_HOR_SPEED, FLOP_HOR_SPEED), + }) + end + end + + self:set_animation( "stand", true) + + return + elseif self.state == "flop" then + self.state = "stand" + self.object:set_acceleration({x = 0, y = 0, z = 0}) + self:set_velocity(0) + end + end +end + +function mob_class:go_to_pos(b) + if not self then return end + local s=self.object:get_pos() + if not b then + --self.state = "stand" + return end + if vector.distance(b,s) < 1 then + --self:set_velocity(0) + return true + end + local v = { x = b.x - s.x, z = b.z - s.z } + local yaw = (atann(v.z / v.x) +math.pi/ 2) - self.rotate + if b.x > s.x then yaw = yaw +math.pi end + self.object:set_yaw(yaw) + self:set_velocity(self.follow_velocity) + self:set_animation("walk") +end + +function mob_class:interact_with_door(action, target) + local p = self.object:get_pos() + --local t = minetest.get_timeofday() + --local dd = minetest.find_nodes_in_area(vector.offset(p,-1,-1,-1),vector.offset(p,1,1,1),{"group:door"}) + --for _,d in pairs(dd) do + if target then + mcl_log("Door target is: ".. minetest.pos_to_string(target)) + + local n = minetest.get_node(target) + if n.name:find("_b_") or n.name:find("_t_") then + mcl_log("Door") + local def = minetest.registered_nodes[n.name] + local closed = n.name:find("_b_1") or n.name:find("_t_1") + --if self.state == PATHFINDING then + if closed and action == "open" and def.on_rightclick then + mcl_log("Open door") + def.on_rightclick(target,n,self) + end + if not closed and action == "close" and def.on_rightclick then + mcl_log("Close door") + def.on_rightclick(target,n,self) + end + --else + else + mcl_log("Not door") + end + else + mcl_log("no target. cannot try and open or close door") + end + --end +end + +function mob_class:do_pathfind_action(action) + if action then + mcl_log("Action present") + local type = action["type"] + local action_val = action["action"] + local target = action["target"] + if target then + mcl_log("Target: ".. minetest.pos_to_string(target)) + end + if type and type == "door" then + mcl_log("Type is door") + self:interact_with_door(action_val, target) + end + end +end + +function mob_class:teleport(target) + if self.do_teleport then + if self.do_teleport(self, target) == false then + return + end + end +end