local helper = adaptive_ai.helper local chat_log = function(s) minetest.chat_send_player("singleplayer", s) end adaptive_ai.chat_log = chat_log local function get_node_group(name, group) if type(group) == "table" then local check = false for _,g in ipairs(group) do check = check or get_node_group(name, g) end return check else if not minetest.registered_nodes[name] or not minetest.registered_nodes[name].groups[group] then return nil end return minetest.registered_nodes[name].groups[group] end end local function is_airlike(name, airlike_groups) airlike_groups = airlike_groups or {} if name == "air" then return true end return get_node_group(name, airlike_groups) end local function search_surface(pos, airlike_groups) local top = minetest.get_node_or_nil(pos) if not top then return nil end local top_pos = {x=pos.x, y=pos.y, z=pos.z} top = minetest.get_node_or_nil(top_pos) --chat_log("Found "..top.name.."...") if not is_airlike(top.name) then repeat top_pos.y = top_pos.y + 1 top = minetest.get_node_or_nil(top_pos) --chat_log("Going up - found "..top.name.."...") until not top or is_airlike(top.name) top_pos = not top and nil or top_pos else repeat top_pos.y = top_pos.y - 1 top = minetest.get_node(top_pos) --chat_log("Going down - found "..top.name.."...") until not top or not is_airlike(top.name) top_pos = not top and nil or top_pos top_pos.y = top_pos.y + 1 end --chat_log("Found!") return top_pos end helper.get_node_group = get_node_group helper.search_surface = search_surface ---------------------------------------------- -- Auxiliar functions copied from mobs_redo -- ---------------------------------------------- -- Localize math functions local pi = math.pi local square = math.sqrt local sin = math.sin local cos = math.cos local abs = math.abs local min = math.min local max = math.max local atann = math.atan local floor = math.floor local random = math.random local atan = function(x) if not x or x ~= x then --error("atan bassed NaN") return 0 else return atann(x) end end -- config values local show_health = minetest.settings:get_bool("mob_show_health") ~= false local use_cmi = minetest.global_exists("cmi") local mobs_drop_items = minetest.settings:get_bool("mobs_drop_items") ~= false local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false -- default nodes local node_fire = "fire:basic_flame" local node_permanent_flame = "fire:permanent_flame" local node_ice = "default:ice" local node_snowblock = "default:snowblock" local node_snow = "default:snow" helper.mobs = {} local get_distance = function(a, b) local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z return square(x * x + y * y + z * z) end local set_animation = function(ent, anim) mobs:set_animation(ent,anim) end local mob_sound = function(ent, sound) if sound then minetest.sound_play(sound, { object = ent.object, gain = 1.0, max_hear_distance = ent.sounds.distance }) end end local set_velocity = function(ent, v) -- do not move if mob has stopped if ent.state == "stand" then ent.object:setvelocity({x = 0, y = 0, z = 0}) return end local yaw = (ent.object:get_yaw() or 0) + ent.rotate ent.object:setvelocity({ x = sin(yaw) * -v, y = ent.object:getvelocity().y, z = cos(yaw) * v }) end local get_velocity = function(ent) local v = ent.object:getvelocity() return (v.x * v.x + v.z * v.z) ^ 0.5 end local node_ok = function(pos, fallback) fallback = fallback or 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 local set_yaw = function(ent, yaw, delay) mobs:yaw(ent, yaw, delay) return ent.target_yaw end local do_jump = function(self) if not self.jump or self.jump_height == 0 or self.fly or self.child or self.order == "stand" then return false end self.facing_fence = false -- something stopping us while moving? if self.state ~= "stand" and get_velocity(self) > 0.5 and self.object:getvelocity().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 -- where is front local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5) local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5) -- what is in front of mob? local nod = node_ok({ x = pos.x + dir_x, y = pos.y + 0.5, z = pos.z + dir_z }) -- thin blocks that do not need to be jumped if nod.name == node_snow then return false end if minetest.registered_items[nod.name].walkable then if not nod.name:find("fence") and not nod.name:find("gate") then local v = self.object:getvelocity() v.y = self.jump_height set_animation(self, "jump") -- only when defined self.object:setvelocity(v) -- when in air move forward minetest.after(0.3, function(self, v) if self.object:get_luaentity() then self.object:set_acceleration({ x = v.x * 2,--1.5, y = 0, z = v.z * 2,--1.5 }) end end, self, v) if get_velocity(self) > 0 then mob_sound(self, self.sounds.jump) end else self.facing_fence = true end return true end return false end local is_at_cliff = function(ent) if ent.fear_height == 0 then -- 0 for no falling protection! return false end local yaw = ent.object:get_yaw() local dir_x = -sin(yaw) * (ent.collisionbox[4] + 0.5) local dir_z = cos(yaw) * (ent.collisionbox[4] + 0.5) local pos = ent.object:get_pos() local ypos = pos.y + ent.collisionbox[2] -- just above floor if minetest.line_of_sight( {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, {x = pos.x + dir_x, y = ypos - ent.fear_height, z = pos.z + dir_z} , 1) then return true end return false end local within_limits = function(pos, radius) if (pos.x - radius) > -30913 and (pos.x + radius) < 30928 and (pos.y - radius) > -30913 and (pos.y + radius) < 30928 and (pos.z - radius) > -30913 and (pos.z + radius) < 30928 then return true -- within limits end return false -- beyond limits end -- update nametag colour local update_tag = function(ent) local col = "#00FF00" local qua = ent.hp_max / 4 if ent.health <= floor(qua) then col = "#FF0000" elseif ent.health <= floor(qua * 2) then col = "#FF6600" elseif ent.health <= floor(qua * 3) then col = "#FFFF00" end ent.object:set_properties({nametag = ent.nametag, nametag_color = col}) end -- check line of sight (BrunoMine) local line_of_sight = function(ent, pos1, pos2, stepsize) stepsize = stepsize or 1 local s, pos = minetest.line_of_sight(pos1, pos2, stepsize) -- normal walking and flying mobs can see you through air if s == true then return true end -- New pos1 to be analyzed local npos1 = {x = pos1.x, y = pos1.y, z = pos1.z} local r, pos = minetest.line_of_sight(npos1, pos2, stepsize) -- Checks the return if r == true then return true end -- Nodename found local nn = minetest.get_node(pos).name -- Target Distance (td) to travel local td = get_distance(pos1, pos2) -- Actual Distance (ad) traveled local ad = 0 -- It continues to advance in the line of sight in search of a real -- obstruction which counts as 'normal' nodebox. while minetest.registered_nodes[nn] and (minetest.registered_nodes[nn].walkable == false or minetest.registered_nodes[nn].drawtype == "nodebox") do -- Check if you can still move forward if td < ad + stepsize then return true -- Reached the target end -- Moves the analyzed pos local d = get_distance(pos1, pos2) npos1.x = ((pos2.x - pos1.x) / d * stepsize) + pos1.x npos1.y = ((pos2.y - pos1.y) / d * stepsize) + pos1.y npos1.z = ((pos2.z - pos1.z) / d * stepsize) + pos1.z -- NaN checks if d == 0 or npos1.x ~= npos1.x or npos1.y ~= npos1.y or npos1.z ~= npos1.z then return false end ad = ad + stepsize -- scan again r, pos = minetest.line_of_sight(npos1, pos2, stepsize) if r == true then return true end -- New Nodename found nn = minetest.get_node(pos).name end return false end -- are we flying in what we are suppose to? (taikedz) local flight_check = function(ent, pos_w) local nod = ent.standing_in local def = minetest.registered_nodes[nod] if not def then return false end -- nil check if type(ent.fly_in) == "string" and nod == ent.fly_in then return true elseif type(ent.fly_in) == "table" then for _,fly_in in pairs(ent.fly_in) do if nod == fly_in then return true end end end -- stops mobs getting stuck inside stairs and plantlike nodes if def.drawtype ~= "airlike" and def.drawtype ~= "liquid" and def.drawtype ~= "flowingliquid" then return true end return false end local effect = function(pos, amount, texture, min_size, max_size, radius, gravity, glow) radius = radius or 2 min_size = min_size or 0.5 max_size = max_size or 1 gravity = gravity or -10 glow = glow or 0 minetest.add_particlespawner({ amount = amount, time = 0.25, minpos = pos, maxpos = pos, minvel = {x = -radius, y = -radius, z = -radius}, maxvel = {x = radius, y = radius, z = radius}, minacc = {x = 0, y = gravity, z = 0}, maxacc = {x = 0, y = gravity, z = 0}, minexptime = 0.1, maxexptime = 1, minsize = min_size, maxsize = max_size, texture = texture, glow = glow, }) end -- drop items local item_drop = function(ent, cooked) -- no drops if disabled by setting if not mobs_drop_items then return end -- no drops for child mobs if ent.child then return end local obj, item, num local pos = ent.object:get_pos() ent.drops = ent.drops or {} -- nil check for n = 1, #ent.drops do if random(1, ent.drops[n].chance) == 1 then num = random(ent.drops[n].min or 1, ent.drops[n].max or 1) item = ent.drops[n].name -- cook items when true if cooked then local output = minetest.get_craft_result({ method = "cooking", width = 1, items = {item}}) if output and output.item and not output.item:is_empty() then item = output.item:get_name() end end -- add item if it exists obj = minetest.add_item(pos, ItemStack(item .. " " .. num)) if obj and obj:get_luaentity() then obj:setvelocity({ x = random(-10, 10) / 9, y = 6, z = random(-10, 10) / 9, }) elseif obj then obj:remove() -- item does not exist end end end ent.drops = {} end -- check if mob is dead or only hurt local check_for_death = function(ent, cause, cmi_cause) -- has health actually changed? if ent.health == ent.old_health and ent.health > 0 then return end ent.old_health = ent.health -- still got some health? play hurt sound if ent.health > 0 then mob_sound(ent, ent.sounds.damage) -- make sure health isn't higher than max if ent.health > ent.hp_max then ent.health = ent.hp_max end -- backup nametag so we can show health stats if not ent.nametag2 then ent.nametag2 = ent.nametag or "" end if show_health and (cmi_cause and cmi_cause.type == "punch") then ent.htimer = 2 ent.nametag = "♥ " .. ent.health .. " / " .. ent.hp_max update_tag(ent) end return false end -- dropped cooked item if mob died in lava if cause == "lava" then item_drop(ent, true) else item_drop(ent, nil) end mob_sound(ent, ent.sounds.death) local pos = ent.object:get_pos() -- execute custom death function if ent.on_die then ent.on_die(ent, pos) if use_cmi then cmi.notify_die(ent.object, cmi_cause) end ent.object:remove() return true end -- default death function and die animation (if defined) if ent.animation and ent.animation.die_start and ent.animation.die_end then local frames = ent.animation.die_end - ent.animation.die_start local speed = ent.animation.die_speed or 15 local length = max(frames / speed, 0) ent.attack = nil ent.v_start = false ent.timer = 0 ent.blinktimer = 0 ent.passive = true ent.state = "die" set_velocity(ent, 0) set_animation(ent, "die") minetest.after(length, function(ent) if use_cmi then cmi.notify_die(ent.object, cmi_cause) end ent.object:remove() end, ent) else if use_cmi then cmi.notify_die(ent.object, cmi_cause) end ent.object:remove() end effect(pos, 20, "tnt_smoke.png") return true end local do_env_damage = function(ent) -- feed/tame text timer (so mob 'full' messages dont spam chat) if ent.htimer > 0 then ent.htimer = ent.htimer - 1 end -- reset nametag after showing health stats if ent.htimer < 1 and ent.nametag2 then ent.nametag = ent.nametag2 ent.nametag2 = nil update_tag(ent) end local pos = ent.object:get_pos() ent.time_of_day = minetest.get_timeofday() -- remove mob if beyond map limits if not within_limits(pos, 0) then ent.object:remove() return end -- bright light harms mob if ent.light_damage ~= 0 -- and pos.y > 0 -- and ent.time_of_day > 0.2 -- and ent.time_of_day < 0.8 and (minetest.get_node_light(pos) or 0) > 12 then ent.health = ent.health - ent.light_damage effect(pos, 5, "tnt_smoke.png") if check_for_death(ent, "light", {type = "light"}) then return end end local y_level = ent.collisionbox[2] if ent.child then y_level = ent.collisionbox[2] * 0.5 end -- what is mob standing in? pos.y = pos.y + y_level + 0.25 -- foot level ent.standing_in = node_ok(pos, "air").name -- print ("standing in " .. ent.standing_in) -- don't fall when on ignore, just stand still if ent.standing_in == "ignore" then ent.object:setvelocity({x = 0, y = 0, z = 0}) end local nodef = minetest.registered_nodes[ent.standing_in] pos.y = pos.y + 1 -- for particle effect position -- water if ent.water_damage and nodef.groups.water then if ent.water_damage ~= 0 then ent.health = ent.health - ent.water_damage effect(pos, 5, "bubble.png", nil, nil, 1, nil) if check_for_death(ent, "water", {type = "environment", pos = pos, node = ent.standing_in}) then return end end -- lava or fire elseif ent.lava_damage and (nodef.groups.lava or ent.standing_in == node_fire or ent.standing_in == node_permanent_flame) then if ent.lava_damage ~= 0 then ent.health = ent.health - ent.lava_damage effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) if check_for_death(ent, "lava", {type = "environment", pos = pos, node = ent.standing_in}) then return end end -- damage_per_second node check elseif nodef.damage_per_second ~= 0 then ent.health = ent.health - nodef.damage_per_second effect(pos, 5, "tnt_smoke.png") if check_for_death(ent, "dps", {type = "environment", pos = pos, node = ent.standing_in}) then return end end check_for_death(ent, "", {type = "unknown"}) end -- find and replace what mob is looking for (grass, wheat etc.) local replace = function(ent, pos) if not mobs_griefing or not ent.replace_rate or not ent.replace_what or ent.child == true or ent.object:getvelocity().y ~= 0 or random(1, ent.replace_rate) > 1 then return end local what, with, y_offset if type(ent.replace_what[1]) == "table" then local num = random(#ent.replace_what) what = ent.replace_what[num][1] or "" with = ent.replace_what[num][2] or "" y_offset = ent.replace_what[num][3] or 0 else what = ent.replace_what with = ent.replace_with or "" y_offset = ent.replace_offset or 0 end pos.y = pos.y + y_offset if #minetest.find_nodes_in_area(pos, pos, what) > 0 then local oldnode = {name = what} local newnode = {name = with} local on_replace_return if ent.on_replace then on_replace_return = ent.on_replace(ent, pos, oldnode, newnode) end if on_replace_return ~= false then minetest.set_node(pos, {name = with}) -- when cow/sheep eats grass, replace wool and milk if ent.gotten == true then ent.gotten = false ent.object:set_properties(ent) end end 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(ent) if not ent.runaway_from then return end local s = ent.object:get_pos() local p, sp, dist local player, obj, min_player local type, name = "", "" local min_dist = ent.view_range + 1 local objs = minetest.get_objects_inside_radius(s, ent.view_range) for n = 1, #objs do if objs[n]:is_player() then if mobs.invis[ objs[n]:get_player_name() ] or ent.owner == objs[n]:get_player_name() 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 ~= ent.name and specific_runaway(ent.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 = get_distance(p, s) -- choose closest player/mpb to runaway from if dist < min_dist and line_of_sight(ent, 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 * pi / 2) - ent.rotate if lp.x > s.x then yaw = yaw + pi end yaw = set_yaw(ent, yaw, 4) ent.state = "runaway" ent.runaway_timer = 3 end end local flop = function(ent) -- swimmers flop when out of their element, and swim again when back in if ent.fly then local s = ent.object:get_pos() if not flight_check(ent, s) then ent.state = "flop" ent.object:setvelocity({x = 0, y = -5, z = 0}) set_animation(ent, "stand") return elseif ent.state == "flop" then ent.state = "stand" end end end local do_stand = function(ent, dtime) local yaw = self.object:get_yaw() or 0 if random(1, 4) == 1 then local lp = nil local s = ent.object:get_pos() local objs = minetest.get_objects_inside_radius(s, 3) for n = 1, #objs do if objs[n]:is_player() then lp = objs[n]:get_pos() break end end -- look at any players nearby, otherwise turn randomly if lp then local vec = { x = lp.x - s.x, z = lp.z - s.z } yaw = (atan(vec.z / vec.x) + pi / 2) - ent.rotate if lp.x > s.x then yaw = yaw + pi end else yaw = yaw + random(-0.5, 0.5) end yaw = set_yaw(ent, yaw, 8) end set_velocity(ent, 0) set_animation(ent, "stand") -- npc's ordered to stand stay standing if ent.type ~= "npc" or ent.order ~= "stand" then if ent.walk_chance ~= 0 and ent.facing_fence ~= true and random(1, 100) <= ent.walk_chance and is_at_cliff(ent) == false then set_velocity(ent, ent.walk_velocity) ent.state = "walk" set_animation(ent, "walk") --[[ fly up/down randomly for flying mobs if ent.fly and random(1, 100) <= ent.walk_chance then local v = ent.object:getvelocity() local ud = random(-1, 2) / 9 ent.object:setvelocity({x = v.x, y = ud, z = v.z}) end--]] end end end local do_walk = function(ent, dtime) local yaw = self.object:get_yaw() or 0 local s = self.object:get_pos() local lp = nil -- is there something I need to avoid? if self.water_damage > 0 and self.lava_damage > 0 then lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"}) elseif self.water_damage > 0 then lp = minetest.find_node_near(s, 1, {"group:water"}) elseif self.lava_damage > 0 then lp = minetest.find_node_near(s, 1, {"group:lava"}) end if lp then -- if mob in water or lava then look for land if (self.lava_damage and minetest.registered_nodes[self.standing_in].groups.lava) or (self.water_damage and minetest.registered_nodes[self.standing_in].groups.water) then lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", "group:sand", node_ice, node_snowblock}) -- did we find land? if lp then local vec = { x = lp.x - s.x, z = lp.z - s.z } yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate if lp.x > s.x then yaw = yaw + pi end -- look towards land and jump/move in that direction yaw = set_yaw(self, yaw, 6) do_jump(self) set_velocity(self, self.walk_velocity) else yaw = yaw + random(-0.5, 0.5) end else local vec = { x = lp.x - s.x, z = lp.z - s.z } yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate if lp.x > s.x then yaw = yaw + pi end end yaw = set_yaw(self, yaw, 8) -- otherwise randomly turn elseif random(1, 100) <= 30 then yaw = yaw + random(-0.5, 0.5) yaw = set_yaw(self, yaw, 8) end -- stand for great fall in front local temp_is_cliff = is_at_cliff(self) if self.facing_fence == true or temp_is_cliff or random(1, 100) <= 30 then set_velocity(self, 0) self.state = "stand" set_animation(self, "stand") else set_velocity(self, self.walk_velocity) if flight_check(self) and self.animation and self.animation.fly_start and self.animation.fly_end then set_animation(self, "fly") else set_animation(self, "walk") end end end -- Config values helper.mobs.show_health = show_health helper.mobs.use_cmi = use_cmi helper.mobs.mobs_griefing = mobs_griefing -- Auxiliar functions helper.mobs.get_distance = get_distance helper.mobs.set_animation = set_animation helper.mobs.mob_sound = mob_sound helper.mobs.node_ok = node_ok helper.mobs.set_velocity = set_velocity helper.mobs.get_velocity = get_velocity helper.mobs.is_at_cliff = is_at_cliff helper.mobs.within_limits = within_limits helper.mobs.update_tag = update_tag helper.mobs.flight_check = flight_check helper.mobs.effect = effect helper.mobs.item_drop = item_drop helper.mobs.check_for_death = check_for_death helper.mobs.do_env_damage = do_env_damage helper.mobs.replace = replace helper.mobs.flop = flop helper.mobs.runaway_from = runaway_from helper.mobs.do_jump = do_jump helper.mobs.set_yaw = set_yaw helper.mobs.do_state_stand = do_state_stand adaptive_ai.helper = helper