diff --git a/mods/ENTITIES/mobkit/LICENSE b/mods/ENTITIES/mobkit/LICENSE new file mode 100644 index 000000000..664fc8320 --- /dev/null +++ b/mods/ENTITIES/mobkit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 TheTermos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mods/ENTITIES/mobkit/README.md b/mods/ENTITIES/mobkit/README.md new file mode 100644 index 000000000..26319f99f --- /dev/null +++ b/mods/ENTITIES/mobkit/README.md @@ -0,0 +1,6 @@ +# mobkit +Entity API for Minetest + +This library is meant to be shared between mods
+Please do not write to the mobkit namespace ('mobkit' global table),
+nor include own copies of mobkit in your mods and modpacks. diff --git a/mods/ENTITIES/mobkit/behaviors2override.lua b/mods/ENTITIES/mobkit/behaviors2override.lua new file mode 100644 index 000000000..1ddd0fbf3 --- /dev/null +++ b/mods/ENTITIES/mobkit/behaviors2override.lua @@ -0,0 +1,830 @@ +local abs = math.abs +local pi = math.pi +local floor = math.floor +local ceil = math.ceil +local random = math.random +local sqrt = math.sqrt +local max = math.max +local min = math.min +local tan = math.tan +local pow = math.pow +local dbg = minetest.chat_send_all + +local abr = tonumber(minetest.get_mapgen_setting('active_block_range')) or 3 + +local neighbors ={ + {x=1,z=0}, + {x=1,z=1}, + {x=0,z=1}, + {x=-1,z=1}, + {x=-1,z=0}, + {x=-1,z=-1}, + {x=0,z=-1}, + {x=1,z=-1} + } + +function [yournamespace].dir2neighbor(dir) + dir.y=0 + dir=vector.round(vector.normalize(dir)) + for k,v in ipairs(neighbors) do + if v.x == dir.x and v.z == dir.z then return k end + end + return 1 +end + +function [yournamespace].neighbor_shift(neighbor,shift) -- int shift: minus is left, plus is right + return (8+neighbor+shift-1)%8+1 +end + +function [yournamespace].is_neighbor_node_reachable(self,neighbor) -- todo: take either number or pos + local offset = neighbors[neighbor] + local pos=mobkit.get_stand_pos(self) + local tpos = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) + local recursteps = ceil(self.jump_height)+1 + local height, liquidflag = mobkit.get_terrain_height(tpos,recursteps) + + if height and abs(height-pos.y) <= self.jump_height then + tpos.y = height + height = height - pos.y + + -- don't cut corners + if neighbor % 2 == 0 then -- diagonal neighbors are even + local n2 = neighbor-1 -- left neighbor never < 0 + offset = neighbors[n2] + local t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) + local h2 = mobkit.get_terrain_height(t2,recursteps) + if h2 and h2 - pos.y > 0.02 then return end + n2 = (neighbor+1)%8 -- right neighbor + offset = neighbors[n2] + t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) + h2 = mobkit.get_terrain_height(t2,recursteps) + if h2 and h2 - pos.y > 0.02 then return end + end + + -- check headroom + if tpos.y+self.height-pos.y > 1 then -- if head in next node above, else no point checking headroom + local snpos = mobkit.get_node_pos(pos) + local pos1 = {x=pos.x,y=snpos.y+1,z=pos.z} -- current pos plus node up + local pos2 = {x=tpos.x,y=tpos.y+self.height,z=tpos.z} -- target head pos + + local nodes = mobkit.get_nodes_in_area(pos1,pos2,true) + + for p,node in pairs(nodes) do + if snpos.x==p.x and snpos.z==p.z then + if node.name=='ignore' or node.walkable then return end + else + if node.name=='ignore' or + (node.walkable and mobkit.get_node_height(p)>tpos.y+0.001) then return end + end + end + end + + return height, tpos, liquidflag + else + return + end +end + +function [yournamespace].get_next_waypoint(self,tpos) + local pos = mobkit.get_stand_pos(self) + local dir=vector.direction(pos,tpos) + local neighbor = [yournamespace].dir2neighbor(dir) + local function update_pos_history(self,pos) + table.insert(self.pos_history,1,pos) + if #self.pos_history > 2 then table.remove(self.pos_history,#self.pos_history) end + end + local nogopos = self.pos_history[2] + + local height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) + if height and not liquidflag + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + + local heightl = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-1)) + if heightl and abs(heightl-height)<0.001 then + local heightr = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,1)) + if heightr and abs(heightr-height)<0.001 then + dir.y = 0 + local dirn = vector.normalize(dir) + local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) + local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) + pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) + end + end + update_pos_history(self,pos2) + return height, pos2 + else + + for i=1,3 do + -- scan left + local height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-i*self.path_dir)) + if height and not liq + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + -- scan right + height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,i*self.path_dir)) + if height and not liq + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + end + --scan rear + height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,4)) + if height and not liquidflag + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + end + -- stuck condition here + table.remove(self.pos_history,2) + self.path_dir = self.path_dir*-1 -- subtle change in pathfinding +end + +function [yournamespace].get_next_waypoint_fast(self,tpos,nogopos) + local pos = mobkit.get_stand_pos(self) + local dir=vector.direction(pos,tpos) + local neighbor = [yournamespace].dir2neighbor(dir) + local height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) + + if height and not liquidflag then + local fast = false + heightl = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-1)) + if heightl and abs(heightl-height)<0.001 then + heightr = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,1)) + if heightr and abs(heightr-height)<0.001 then + fast = true + dir.y = 0 + local dirn = vector.normalize(dir) + local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) + local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) + pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) + end + end + return height, pos2, fast + else + + for i=1,4 do + -- scan left + height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-i)) + if height and not liq then return height,pos2 end + -- scan right + height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,i)) + if height and not liq then return height,pos2 end + end + end +end + +function [yournamespace].goto_next_waypoint(self,tpos) + local height, pos2 = [yournamespace].get_next_waypoint(self,tpos) + + if not height then return false end + + if height <= 0.01 then + local yaw = self.object:get_yaw() + local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2)) + if abs(tyaw-yaw) > 1 then + [yournamespace].lq_turn2pos(self,pos2) + end + [yournamespace].lq_dumbwalk(self,pos2) + else + [yournamespace].lq_turn2pos(self,pos2) + [yournamespace].lq_dumbjump(self,height) + end + return true +end + +---------------------------- +-- BEHAVIORS +---------------------------- +-- LOW LEVEL QUEUE FUNCTIONS +---------------------------- + +function [yournamespace].lq_turn2pos(self,tpos) + local func=function(self) + local pos = self.object:get_pos() + return mobkit.turn2yaw(self, + minetest.dir_to_yaw(vector.direction(pos,tpos))) + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_idle(self,duration,anim) + anim = anim or 'stand' + local init = true + local func=function(self) + if init then + mobkit.animate(self,anim) + init=false + end + duration = duration-self.dtime + if duration <= 0 then return true end + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_dumbwalk(self,dest,speed_factor) + local timer = 3 -- failsafe + speed_factor = speed_factor or 1 + local func=function(self) + mobkit.animate(self,'walk') + timer = timer - self.dtime + if timer < 0 then return true end + + local pos = mobkit.get_stand_pos(self) + local y = self.object:get_velocity().y + + if mobkit.is_there_yet2d(pos,minetest.yaw_to_dir(self.object:get_yaw()),dest) then +-- if mobkit.isnear2d(pos,dest,0.25) then + if not self.isonground or abs(dest.y-pos.y) > 0.1 then -- prevent uncontrolled fall when velocity too high +-- if abs(dest.y-pos.y) > 0.1 then -- isonground too slow for speeds > 4 + self.object:set_velocity({x=0,y=y,z=0}) + end + return true + end + + if self.isonground then + local dir = vector.normalize(vector.direction({x=pos.x,y=0,z=pos.z}, + {x=dest.x,y=0,z=dest.z})) + dir = vector.multiply(dir,self.max_speed*speed_factor) +-- self.object:set_yaw(minetest.dir_to_yaw(dir)) + mobkit.turn2yaw(self,minetest.dir_to_yaw(dir)) + dir.y = y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +-- initial velocity for jump height h, v= a*sqrt(h*2/a) ,add 20% +function [yournamespace].lq_dumbjump(self,height,anim) + anim = anim or 'stand' + local jump = true + local func=function(self) + local yaw = self.object:get_yaw() + if self.isonground then + if jump then + mobkit.animate(self,anim) + local dir = minetest.yaw_to_dir(yaw) + dir.y = -mobkit.gravity*sqrt((height+0.35)*2/-mobkit.gravity) + self.object:set_velocity(dir) + jump = false + else -- the eagle has landed + return true + end + else + local dir = minetest.yaw_to_dir(yaw) + local vel = self.object:get_velocity() + if self.lastvelocity.y < 0.9 then + dir = vector.multiply(dir,3) + end + dir.y = vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_jumpout(self) + local phase = 1 + local func=function(self) + local vel=self.object:get_velocity() + if phase == 1 then + vel.y=vel.y+5 + self.object:set_velocity(vel) + phase = 2 + else + if vel.y < 0 then return true end + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir.y=vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_freejump(self) + local phase = 1 + local func=function(self) + local vel=self.object:get_velocity() + if phase == 1 then + vel.y=vel.y+6 + self.object:set_velocity(vel) + phase = 2 + else + if vel.y <= 0.01 then return true end + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir.y=vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_jumpattack(self,height,target) + local init=true + local timer=0.5 + local tgtbox = target:get_properties().collisionbox + local func=function(self) + if not mobkit.is_alive(target) then return true end + if self.isonground then + if init then -- collision bug workaround + local vel = self.object:get_velocity() + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir=vector.multiply(dir,6) + dir.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) + self.object:set_velocity(dir) + mobkit.make_sound(self,'charge') + init=false + else + [yournamespace].lq_idle(self,0.3) + return true + end + else + local tgtpos = target:get_pos() + local pos = self.object:get_pos() + -- calculate attack spot + local yaw = self.object:get_yaw() + local dir = minetest.yaw_to_dir(yaw) + local apos = mobkit.pos_translate2d(pos,yaw,self.attack.range) + + if mobkit.is_pos_in_box(apos,tgtpos,tgtbox) then --bite + target:punch(self.object,1,self.attack) + -- bounce off + local vy = self.object:get_velocity().y + self.object:set_velocity({x=dir.x*-3,y=vy,z=dir.z*-3}) + -- play attack sound if defined + mobkit.make_sound(self,'attack') + return true + end + end + end + mobkit.queue_low(self,func) +end + +function [yournamespace].lq_fallover(self) + local zrot = 0 + local init = true + local func=function(self) + if init then + local vel = self.object:get_velocity() + self.object:set_velocity(mobkit.pos_shift(vel,{y=1})) + mobkit.animate(self,'stand') + init = false + end + zrot=zrot+pi*0.05 + local rot = self.object:get_rotation() + self.object:set_rotation({x=rot.x,y=rot.y,z=zrot}) + if zrot >= pi*0.5 then return true end + end + mobkit.queue_low(self,func) +end +----------------------------- +-- HIGH LEVEL QUEUE FUNCTIONS +----------------------------- + +function [yournamespace].dumbstep(self,height,tpos,speed_factor,idle_duration) + if height <= 0.001 then + [yournamespace].lq_turn2pos(self,tpos) + [yournamespace].lq_dumbwalk(self,tpos,speed_factor) + else + [yournamespace].lq_turn2pos(self,tpos) + [yournamespace].lq_dumbjump(self,height) + end + idle_duration = idle_duration or 6 + [yournamespace].lq_idle(self,random(ceil(idle_duration*0.5),idle_duration)) +end + +function [yournamespace].hq_roam(self,prty) + local func=function(self) + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local neighbor = random(8) + + local height, tpos, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) + if height and not liquidflag then [yournamespace].dumbstep(self,height,tpos,0.3) end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_follow0(self,tgtobj) -- probably delete this one + local func = function(self) + if not tgtobj then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) > 3 then + local neighbor = [yournamespace].dir2neighbor(vector.direction(pos,opos)) +if not neighbor then return true end --temp debug + local height, tpos = [yournamespace].is_neighbor_node_reachable(self,neighbor) + if height then [yournamespace].dumbstep(self,height,tpos) + else + for i=1,4 do --scan left + height, tpos = [yournamespace].is_neighbor_node_reachable(self,(8+neighbor-i-1)%8+1) + if height then [yournamespace].dumbstep(self,height,tpos) + break + end --scan right + height, tpos = [yournamespace].is_neighbor_node_reachable(self,(neighbor+i-1)%8+1) + if height then [yournamespace].dumbstep(self,height,tpos) + break + end + end + end + else + [yournamespace].lq_idle(self,1) + end + end + end + mobkit.queue_high(self,func,0) +end + +function [yournamespace].hq_follow(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) > 3 then + [yournamespace].goto_next_waypoint(self,opos) + else + [yournamespace].lq_idle(self,1) + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_goto(self,prty,tpos) + local func = function(self) + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + if vector.distance(pos,tpos) > 3 then + [yournamespace].goto_next_waypoint(self,tpos) + else + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_runfrom(self,prty,tgtobj) + local init=true + local timer=6 + local func = function(self) + + if not mobkit.is_alive(tgtobj) then return true end + if init then + timer = timer-self.dtime + if timer <=0 or vector.distance(self.object:get_pos(),tgtobj:get_pos()) < 8 then + mobkit.make_sound(self,'scared') + init=false + end + return + end + + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) < self.view_range*1.1 then + local tpos = {x=2*pos.x - opos.x, + y=opos.y, + z=2*pos.z - opos.z} + [yournamespace].goto_next_waypoint(self,tpos) + else + self.object:set_velocity({x=0,y=0,z=0}) + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_hunt(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + local dist = vector.distance(pos,opos) + if dist > self.view_range then + return true + elseif dist > 3 then + [yournamespace].goto_next_waypoint(self,opos) + else + [yournamespace].hq_attack(self,prty+1,tgtobj) + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_warn(self,prty,tgtobj) + local timer=0 + local tgttime = 0 + local init = true + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if init then + mobkit.animate(self,'stand') + init = false + end + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + local dist = vector.distance(pos,opos) + + if dist > 11 then + return true + elseif dist < 4 or timer > 12 then -- too close man +-- mobkit.clear_queue_high(self) + mobkit.remember(self,'hate',tgtobj:get_player_name()) + [yournamespace].hq_hunt(self,prty+1,tgtobj) -- priority + else + timer = timer+self.dtime + if mobkit.is_queue_empty_low(self) then + [yournamespace].lq_turn2pos(self,opos) + end + -- make noise in random intervals + if timer > tgttime then + mobkit.make_sound(self,'warn') + -- if self.sounds and self.sounds.warn then + -- minetest.sound_play(self.sounds.warn, {object=self.object}) + -- end + tgttime = timer + 1.1 + random()*1.5 + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_die(self) + local timer = 5 + local start = true + local func = function(self) + if start then + [yournamespace].lq_fallover(self) + self.logic = function(self) end -- brain dead as well + start=false + end + timer = timer-self.dtime + if timer < 0 then self.object:remove() end + end + mobkit.queue_high(self,func,100) +end + +function [yournamespace].hq_attack(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) then + local pos = mobkit.get_stand_pos(self) +-- local tpos = tgtobj:get_pos() + local tpos = mobkit.get_stand_pos(tgtobj) + local dist = vector.distance(pos,tpos) + if dist > 3 then + return true + else + [yournamespace].lq_turn2pos(self,tpos) + local height = tgtobj:is_player() and 0.35 or tgtobj:get_luaentity().height*0.6 + if tpos.y+height>pos.y then + [yournamespace].lq_jumpattack(self,tpos.y+height-pos.y,tgtobj) + else + [yournamespace].lq_dumbwalk(self,mobkit.pos_shift(tpos,{x=random()-0.5,z=random()-0.5})) + end + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_liquid_recovery(self,prty) -- scan for nearest land + local radius = 1 + local yaw = 0 + local func = function(self) + if not self.isinliquid then return true end + local pos=self.object:get_pos() + local vec = minetest.yaw_to_dir(yaw) + local pos2 = mobkit.pos_shift(pos,vector.multiply(vec,radius)) + local height, liquidflag = mobkit.get_terrain_height(pos2) + if height and not liquidflag then + [yournamespace].hq_swimto(self,prty,pos2) + return true + end + yaw=yaw+pi*0.25 + if yaw>2*pi then + yaw = 0 + radius=radius+1 + if radius > self.view_range then + self.hp = 0 + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_swimto(self,prty,tpos) + local box = self.object:get_properties().collisionbox + local cols = {} + local func = function(self) + if not self.isinliquid then + if self.isonground then return true end + return false + end + + local pos = mobkit.get_stand_pos(self) + local y=self.object:get_velocity().y + local pos2d = {x=pos.x,y=tpos.y,z=pos.z} + local dir=vector.normalize(vector.direction(pos2d,tpos)) + local yaw = minetest.dir_to_yaw(dir) + + if mobkit.timer(self,1) then + cols = mobkit.get_box_displace_cols(pos,box,dir,1) + for _,p in ipairs(cols[1]) do + p.y=pos.y + local h,l = mobkit.get_terrain_height(p) + if h and h>pos.y and self.isinliquid then + [yournamespace].lq_freejump(self) + break + end + end + elseif mobkit.turn2yaw(self,yaw) then + dir.y = y + self.object:set_velocity(dir) + end + end + mobkit.queue_high(self,func,prty) +end + +--------------------- +-- AQUATIC +--------------------- + +-- MACROS +local function aqua_radar_dumb(pos,yaw,range,reverse) + range = range or 4 + + local function okpos(p) + local node = mobkit.nodeatpos(p) + if node then + if node.drawtype == 'liquid' then + local nodeu = mobkit.nodeatpos(mobkit.pos_shift(p,{y=1})) + local noded = mobkit.nodeatpos(mobkit.pos_shift(p,{y=-1})) + if (nodeu and nodeu.drawtype == 'liquid') or (noded and noded.drawtype == 'liquid') then + return true + else + return false + end + else + local h,l = mobkit.get_terrain_height(p) + if h then + local node2 = mobkit.nodeatpos({x=p.x,y=h+1.99,z=p.z}) + if node2 and node2.drawtype == 'liquid' then return true, h end + else + return false + end + end + else + return false + end + end + + local fpos = mobkit.pos_translate2d(pos,yaw,range) + local ok,h = okpos(fpos) + if not ok then + local ffrom, fto, fstep + if reverse then + ffrom, fto, fstep = 3,1,-1 + else + ffrom, fto, fstep = 1,3,1 + end + for i=ffrom, fto, fstep do + local ok,h = okpos(mobkit.pos_translate2d(pos,yaw+i,range)) + if ok then return yaw+i,h end + ok,h = okpos(mobkit.pos_translate2d(pos,yaw-i,range)) + if ok then return yaw-i,h end + end + return yaw+pi,h + else + return yaw, h + end +end + +function [yournamespace].is_in_deep(target) + if not target then return false end + local nodepos = mobkit.get_stand_pos(target) + local node1 = mobkit.nodeatpos(nodepos) + nodepos.y=nodepos.y+1 + local node2 = mobkit.nodeatpos(nodepos) + nodepos.y=nodepos.y-2 + local node3 = mobkit.nodeatpos(nodepos) + if node1 and node2 and node3 and node1.drawtype=='liquid' and (node2.drawtype=='liquid' or node3.drawtype=='liquid') then + return true + end +end + +-- HQ behaviors + +function [yournamespace].hq_aqua_roam(self,prty,speed) + local tyaw = 0 + local init = true + local prvscanpos = {x=0,y=0,z=0} + local center = self.object:get_pos() + local func = function(self) + if init then + mobkit.animate(self,'def') + init = false + end + local pos = mobkit.get_stand_pos(self) + local yaw = self.object:get_yaw() + local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) + if not vector.equals(prvscanpos,scanpos) then + prvscanpos=scanpos + local nyaw,height = aqua_radar_dumb(pos,yaw,speed,true) + if height and height > pos.y then + local vel = self.object:get_velocity() + vel.y = vel.y+1 + self.object:set_velocity(vel) + end + if yaw ~= nyaw then + tyaw=nyaw + [yournamespace].hq_aqua_turn(self,prty+1,tyaw,speed) + return + end + end + if mobkit.timer(self,1) then + if vector.distance(pos,center) > abr*16*0.5 then + tyaw = minetest.dir_to_yaw(vector.direction(pos,{x=center.x+random()*10-5,y=center.y,z=center.z+random()*10-5})) + else + if random(10)>=9 then tyaw=tyaw+random()*pi - pi*0.5 end + end + end + + mobkit.turn2yaw(self,tyaw,3) +-- local yaw = self.object:get_yaw() + mobkit.go_forward_horizontal(self,speed) + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_aqua_turn(self,prty,tyaw,speed) + local func = function(self) + local finished=mobkit.turn2yaw(self,tyaw) +-- local yaw = self.object:get_yaw() + mobkit.go_forward_horizontal(self,speed) + if finished then return true end + end + mobkit.queue_high(self,func,prty) +end + +function [yournamespace].hq_aqua_attack(self,prty,tgtobj,speed) + local tyaw = 0 + local prvscanpos = {x=0,y=0,z=0} + local init = true + local tgtbox = tgtobj:get_properties().collisionbox + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if init then + mobkit.animate(self,'fast') + mobkit.make_sound(self,'attack') + init = false + end + local pos = mobkit.get_stand_pos(self) + local yaw = self.object:get_yaw() + local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) + if not vector.equals(prvscanpos,scanpos) then + prvscanpos=scanpos + local nyaw,height = aqua_radar_dumb(pos,yaw,speed*0.5) + if height and height > pos.y then + local vel = self.object:get_velocity() + vel.y = vel.y+1 + self.object:set_velocity(vel) + end + if yaw ~= nyaw then + tyaw=nyaw + [yournamespace].hq_aqua_turn(self,prty+1,tyaw,speed) + return + end + end + + local tpos = tgtobj:get_pos() + local tyaw=minetest.dir_to_yaw(vector.direction(pos,tpos)) + mobkit.turn2yaw(self,tyaw,3) + local yaw = self.object:get_yaw() + if mobkit.timer(self,1) then + if not [yournamespace].is_in_deep(tgtobj) then return true end + local vel = self.object:get_velocity() + if tpos.y>pos.y+0.5 then self.object:set_velocity({x=vel.x,y=vel.y+0.5,z=vel.z}) + elseif tpos.y 0.02 then return end + n2 = (neighbor+1)%8 -- right neighbor + offset = neighbors[n2] + t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) + h2 = mobkit.get_terrain_height(t2,recursteps) + if h2 and h2 - pos.y > 0.02 then return end + end + + -- check headroom + if tpos.y+self.height-pos.y > 1 then -- if head in next node above, else no point checking headroom + local snpos = mobkit.get_node_pos(pos) + local pos1 = {x=pos.x,y=snpos.y+1,z=pos.z} -- current pos plus node up + local pos2 = {x=tpos.x,y=tpos.y+self.height,z=tpos.z} -- target head pos + + local nodes = mobkit.get_nodes_in_area(pos1,pos2,true) + + for p,node in pairs(nodes) do + if snpos.x==p.x and snpos.z==p.z then + if node.name=='ignore' or node.walkable then return end + else + if node.name=='ignore' or + (node.walkable and mobkit.get_node_height(p)>tpos.y+0.001) then return end + end + end + end + + return height, tpos, liquidflag + else + return + end +end + +function mobkit.get_next_waypoint(self,tpos) + local pos = mobkit.get_stand_pos(self) + local dir=vector.direction(pos,tpos) + local neighbor = mobkit.dir2neighbor(dir) + local function update_pos_history(self,pos) + table.insert(self.pos_history,1,pos) + if #self.pos_history > 2 then table.remove(self.pos_history,#self.pos_history) end + end + local nogopos = self.pos_history[2] + + local height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) + if height and not liquidflag + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + + local heightl = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-1)) + if heightl and abs(heightl-height)<0.001 then + local heightr = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,1)) + if heightr and abs(heightr-height)<0.001 then + dir.y = 0 + local dirn = vector.normalize(dir) + local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) + local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) + pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) + end + end + update_pos_history(self,pos2) + return height, pos2 + else + + for i=1,3 do + -- scan left + local height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-i*self.path_dir)) + if height and not liq + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + -- scan right + height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,i*self.path_dir)) + if height and not liq + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + end + --scan rear + height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,4)) + if height and not liquidflag + and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then + update_pos_history(self,pos2) + return height,pos2 + end + end + -- stuck condition here + table.remove(self.pos_history,2) + self.path_dir = self.path_dir*-1 -- subtle change in pathfinding +end + +function mobkit.get_next_waypoint_fast(self,tpos,nogopos) + local pos = mobkit.get_stand_pos(self) + local dir=vector.direction(pos,tpos) + local neighbor = mobkit.dir2neighbor(dir) + local height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) + + if height and not liquidflag then + local fast = false + heightl = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-1)) + if heightl and abs(heightl-height)<0.001 then + heightr = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,1)) + if heightr and abs(heightr-height)<0.001 then + fast = true + dir.y = 0 + local dirn = vector.normalize(dir) + local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) + local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) + pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) + end + end + return height, pos2, fast + else + + for i=1,4 do + -- scan left + height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-i)) + if height and not liq then return height,pos2 end + -- scan right + height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,i)) + if height and not liq then return height,pos2 end + end + end +end + +function mobkit.goto_next_waypoint(self,tpos) + local height, pos2 = mobkit.get_next_waypoint(self,tpos) + + if not height then return false end + + if height <= 0.01 then + local yaw = self.object:get_yaw() + local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2)) + if abs(tyaw-yaw) > 1 then + mobkit.lq_turn2pos(self,pos2) + end + mobkit.lq_dumbwalk(self,pos2) + else + mobkit.lq_turn2pos(self,pos2) + mobkit.lq_dumbjump(self,height) + end + return true +end + +---------------------------- +-- BEHAVIORS +---------------------------- +-- LOW LEVEL QUEUE FUNCTIONS +---------------------------- + +function mobkit.lq_turn2pos(self,tpos) + local func=function(self) + local pos = self.object:get_pos() + return mobkit.turn2yaw(self, + minetest.dir_to_yaw(vector.direction(pos,tpos))) + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_idle(self,duration,anim) + anim = anim or 'stand' + local init = true + local func=function(self) + if init then + mobkit.animate(self,anim) + init=false + end + duration = duration-self.dtime + if duration <= 0 then return true end + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_dumbwalk(self,dest,speed_factor) + local timer = 3 -- failsafe + speed_factor = speed_factor or 1 + local func=function(self) + mobkit.animate(self,'walk') + timer = timer - self.dtime + if timer < 0 then return true end + + local pos = mobkit.get_stand_pos(self) + local y = self.object:get_velocity().y + + if mobkit.is_there_yet2d(pos,minetest.yaw_to_dir(self.object:get_yaw()),dest) then +-- if mobkit.isnear2d(pos,dest,0.25) then + if not self.isonground or abs(dest.y-pos.y) > 0.1 then -- prevent uncontrolled fall when velocity too high +-- if abs(dest.y-pos.y) > 0.1 then -- isonground too slow for speeds > 4 + self.object:set_velocity({x=0,y=y,z=0}) + end + return true + end + + if self.isonground then + local dir = vector.normalize(vector.direction({x=pos.x,y=0,z=pos.z}, + {x=dest.x,y=0,z=dest.z})) + dir = vector.multiply(dir,self.max_speed*speed_factor) +-- self.object:set_yaw(minetest.dir_to_yaw(dir)) + mobkit.turn2yaw(self,minetest.dir_to_yaw(dir)) + dir.y = y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +-- initial velocity for jump height h, v= a*sqrt(h*2/a) ,add 20% +function mobkit.lq_dumbjump(self,height,anim) + anim = anim or 'stand' + local jump = true + local func=function(self) + local yaw = self.object:get_yaw() + if self.isonground then + if jump then + mobkit.animate(self,anim) + local dir = minetest.yaw_to_dir(yaw) + dir.y = -mobkit.gravity*sqrt((height+0.35)*2/-mobkit.gravity) + self.object:set_velocity(dir) + jump = false + else -- the eagle has landed + return true + end + else + local dir = minetest.yaw_to_dir(yaw) + local vel = self.object:get_velocity() + if self.lastvelocity.y < 0.9 then + dir = vector.multiply(dir,3) + end + dir.y = vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_jumpout(self) + local phase = 1 + local func=function(self) + local vel=self.object:get_velocity() + if phase == 1 then + vel.y=vel.y+5 + self.object:set_velocity(vel) + phase = 2 + else + if vel.y < 0 then return true end + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir.y=vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_freejump(self) + local phase = 1 + local func=function(self) + local vel=self.object:get_velocity() + if phase == 1 then + vel.y=vel.y+6 + self.object:set_velocity(vel) + phase = 2 + else + if vel.y <= 0.01 then return true end + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir.y=vel.y + self.object:set_velocity(dir) + end + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_jumpattack(self,height,target) + local init=true + local timer=0.5 + local tgtbox = target:get_properties().collisionbox + local func=function(self) + if not mobkit.is_alive(target) then return true end + if self.isonground then + if init then -- collision bug workaround + local vel = self.object:get_velocity() + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir=vector.multiply(dir,6) + dir.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) + self.object:set_velocity(dir) + mobkit.make_sound(self,'charge') + init=false + else + mobkit.lq_idle(self,0.3) + return true + end + else + local tgtpos = target:get_pos() + local pos = self.object:get_pos() + -- calculate attack spot + local yaw = self.object:get_yaw() + local dir = minetest.yaw_to_dir(yaw) + local apos = mobkit.pos_translate2d(pos,yaw,self.attack.range) + + if mobkit.is_pos_in_box(apos,tgtpos,tgtbox) then --bite + target:punch(self.object,1,self.attack) + -- bounce off + local vy = self.object:get_velocity().y + self.object:set_velocity({x=dir.x*-3,y=vy,z=dir.z*-3}) + -- play attack sound if defined + mobkit.make_sound(self,'attack') + return true + end + end + end + mobkit.queue_low(self,func) +end + +function mobkit.lq_fallover(self) + local zrot = 0 + local init = true + local func=function(self) + if init then + local vel = self.object:get_velocity() + self.object:set_velocity(mobkit.pos_shift(vel,{y=1})) + mobkit.animate(self,'stand') + init = false + end + zrot=zrot+pi*0.05 + local rot = self.object:get_rotation() + self.object:set_rotation({x=rot.x,y=rot.y,z=zrot}) + if zrot >= pi*0.5 then return true end + end + mobkit.queue_low(self,func) +end +----------------------------- +-- HIGH LEVEL QUEUE FUNCTIONS +----------------------------- + +function mobkit.dumbstep(self,height,tpos,speed_factor,idle_duration) + if height <= 0.001 then + mobkit.lq_turn2pos(self,tpos) + mobkit.lq_dumbwalk(self,tpos,speed_factor) + else + mobkit.lq_turn2pos(self,tpos) + mobkit.lq_dumbjump(self,height) + end + idle_duration = idle_duration or 6 + mobkit.lq_idle(self,random(ceil(idle_duration*0.5),idle_duration)) +end + +function mobkit.hq_roam(self,prty) + local func=function(self) + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local neighbor = random(8) + + local height, tpos, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) + if height and not liquidflag then mobkit.dumbstep(self,height,tpos,0.3) end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_follow0(self,tgtobj) -- probably delete this one + local func = function(self) + if not tgtobj then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) > 3 then + local neighbor = mobkit.dir2neighbor(vector.direction(pos,opos)) +if not neighbor then return true end --temp debug + local height, tpos = mobkit.is_neighbor_node_reachable(self,neighbor) + if height then mobkit.dumbstep(self,height,tpos) + else + for i=1,4 do --scan left + height, tpos = mobkit.is_neighbor_node_reachable(self,(8+neighbor-i-1)%8+1) + if height then mobkit.dumbstep(self,height,tpos) + break + end --scan right + height, tpos = mobkit.is_neighbor_node_reachable(self,(neighbor+i-1)%8+1) + if height then mobkit.dumbstep(self,height,tpos) + break + end + end + end + else + mobkit.lq_idle(self,1) + end + end + end + mobkit.queue_high(self,func,0) +end + +function mobkit.hq_follow(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) > 3 then + mobkit.goto_next_waypoint(self,opos) + else + mobkit.lq_idle(self,1) + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_goto(self,prty,tpos) + local func = function(self) + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + if vector.distance(pos,tpos) > 3 then + mobkit.goto_next_waypoint(self,tpos) + else + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_runfrom(self,prty,tgtobj) + local init=true + local timer=6 + local func = function(self) + + if not mobkit.is_alive(tgtobj) then return true end + if init then + timer = timer-self.dtime + if timer <=0 or vector.distance(self.object:get_pos(),tgtobj:get_pos()) < 8 then + mobkit.make_sound(self,'scared') + init=false + end + return + end + + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + if vector.distance(pos,opos) < self.view_range*1.1 then + local tpos = {x=2*pos.x - opos.x, + y=opos.y, + z=2*pos.z - opos.z} + mobkit.goto_next_waypoint(self,tpos) + else + self.object:set_velocity({x=0,y=0,z=0}) + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_hunt(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) and self.isonground then + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + local dist = vector.distance(pos,opos) + if dist > self.view_range then + return true + elseif dist > 3 then + mobkit.goto_next_waypoint(self,opos) + else + mobkit.hq_attack(self,prty+1,tgtobj) + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_warn(self,prty,tgtobj) + local timer=0 + local tgttime = 0 + local init = true + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if init then + mobkit.animate(self,'stand') + init = false + end + local pos = mobkit.get_stand_pos(self) + local opos = tgtobj:get_pos() + local dist = vector.distance(pos,opos) + + if dist > 11 then + return true + elseif dist < 4 or timer > 12 then -- too close man +-- mobkit.clear_queue_high(self) + mobkit.remember(self,'hate',tgtobj:get_player_name()) + mobkit.hq_hunt(self,prty+1,tgtobj) -- priority + else + timer = timer+self.dtime + if mobkit.is_queue_empty_low(self) then + mobkit.lq_turn2pos(self,opos) + end + -- make noise in random intervals + if timer > tgttime then + mobkit.make_sound(self,'warn') + -- if self.sounds and self.sounds.warn then + -- minetest.sound_play(self.sounds.warn, {object=self.object}) + -- end + tgttime = timer + 1.1 + random()*1.5 + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_die(self) + local timer = 5 + local start = true + local func = function(self) + if start then + mobkit.lq_fallover(self) + self.logic = function(self) end -- brain dead as well + start=false + end + timer = timer-self.dtime + if timer < 0 then self.object:remove() end + end + mobkit.queue_high(self,func,100) +end + +function mobkit.hq_attack(self,prty,tgtobj) + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if mobkit.is_queue_empty_low(self) then + local pos = mobkit.get_stand_pos(self) +-- local tpos = tgtobj:get_pos() + local tpos = mobkit.get_stand_pos(tgtobj) + local dist = vector.distance(pos,tpos) + if dist > 3 then + return true + else + mobkit.lq_turn2pos(self,tpos) + local height = tgtobj:is_player() and 0.35 or tgtobj:get_luaentity().height*0.6 + if tpos.y+height>pos.y then + mobkit.lq_jumpattack(self,tpos.y+height-pos.y,tgtobj) + else + mobkit.lq_dumbwalk(self,mobkit.pos_shift(tpos,{x=random()-0.5,z=random()-0.5})) + end + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_liquid_recovery(self,prty) -- scan for nearest land + local radius = 1 + local yaw = 0 + local func = function(self) + if not self.isinliquid then return true end + local pos=self.object:get_pos() + local vec = minetest.yaw_to_dir(yaw) + local pos2 = mobkit.pos_shift(pos,vector.multiply(vec,radius)) + local height, liquidflag = mobkit.get_terrain_height(pos2) + if height and not liquidflag then + mobkit.hq_swimto(self,prty,pos2) + return true + end + yaw=yaw+pi*0.25 + if yaw>2*pi then + yaw = 0 + radius=radius+1 + if radius > self.view_range then + self.hp = 0 + return true + end + end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_swimto(self,prty,tpos) + local box = self.object:get_properties().collisionbox + local cols = {} + local func = function(self) + if not self.isinliquid then + if self.isonground then return true end + return false + end + + local pos = mobkit.get_stand_pos(self) + local y=self.object:get_velocity().y + local pos2d = {x=pos.x,y=tpos.y,z=pos.z} + local dir=vector.normalize(vector.direction(pos2d,tpos)) + local yaw = minetest.dir_to_yaw(dir) + + if mobkit.timer(self,1) then + cols = mobkit.get_box_displace_cols(pos,box,dir,1) + for _,p in ipairs(cols[1]) do + p.y=pos.y + local h,l = mobkit.get_terrain_height(p) + if h and h>pos.y and self.isinliquid then + mobkit.lq_freejump(self) + break + end + end + elseif mobkit.turn2yaw(self,yaw) then + dir.y = y + self.object:set_velocity(dir) + end + end + mobkit.queue_high(self,func,prty) +end + +--------------------- +-- AQUATIC +--------------------- + +-- MACROS +local function aqua_radar_dumb(pos,yaw,range,reverse) + range = range or 4 + + local function okpos(p) + local node = mobkit.nodeatpos(p) + if node then + if node.drawtype == 'liquid' then + local nodeu = mobkit.nodeatpos(mobkit.pos_shift(p,{y=1})) + local noded = mobkit.nodeatpos(mobkit.pos_shift(p,{y=-1})) + if (nodeu and nodeu.drawtype == 'liquid') or (noded and noded.drawtype == 'liquid') then + return true + else + return false + end + else + local h,l = mobkit.get_terrain_height(p) + if h then + local node2 = mobkit.nodeatpos({x=p.x,y=h+1.99,z=p.z}) + if node2 and node2.drawtype == 'liquid' then return true, h end + else + return false + end + end + else + return false + end + end + + local fpos = mobkit.pos_translate2d(pos,yaw,range) + local ok,h = okpos(fpos) + if not ok then + local ffrom, fto, fstep + if reverse then + ffrom, fto, fstep = 3,1,-1 + else + ffrom, fto, fstep = 1,3,1 + end + for i=ffrom, fto, fstep do + local ok,h = okpos(mobkit.pos_translate2d(pos,yaw+i,range)) + if ok then return yaw+i,h end + ok,h = okpos(mobkit.pos_translate2d(pos,yaw-i,range)) + if ok then return yaw-i,h end + end + return yaw+pi,h + else + return yaw, h + end +end + +function mobkit.is_in_deep(target) + if not target then return false end + local nodepos = mobkit.get_stand_pos(target) + local node1 = mobkit.nodeatpos(nodepos) + nodepos.y=nodepos.y+1 + local node2 = mobkit.nodeatpos(nodepos) + nodepos.y=nodepos.y-2 + local node3 = mobkit.nodeatpos(nodepos) + if node1 and node2 and node3 and node1.drawtype=='liquid' and (node2.drawtype=='liquid' or node3.drawtype=='liquid') then + return true + end +end + +-- HQ behaviors + +function mobkit.hq_aqua_roam(self,prty,speed) + local tyaw = 0 + local init = true + local prvscanpos = {x=0,y=0,z=0} + local center = self.object:get_pos() + local func = function(self) + if init then + mobkit.animate(self,'def') + init = false + end + local pos = mobkit.get_stand_pos(self) + local yaw = self.object:get_yaw() + local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) + if not vector.equals(prvscanpos,scanpos) then + prvscanpos=scanpos + local nyaw,height = aqua_radar_dumb(pos,yaw,speed,true) + if height and height > pos.y then + local vel = self.object:get_velocity() + vel.y = vel.y+1 + self.object:set_velocity(vel) + end + if yaw ~= nyaw then + tyaw=nyaw + mobkit.hq_aqua_turn(self,prty+1,tyaw,speed) + return + end + end + if mobkit.timer(self,1) then + if vector.distance(pos,center) > abr*16*0.5 then + tyaw = minetest.dir_to_yaw(vector.direction(pos,{x=center.x+random()*10-5,y=center.y,z=center.z+random()*10-5})) + else + if random(10)>=9 then tyaw=tyaw+random()*pi - pi*0.5 end + end + end + + mobkit.turn2yaw(self,tyaw,3) +-- local yaw = self.object:get_yaw() + mobkit.go_forward_horizontal(self,speed) + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_aqua_turn(self,prty,tyaw,speed) + local func = function(self) + local finished=mobkit.turn2yaw(self,tyaw) +-- local yaw = self.object:get_yaw() + mobkit.go_forward_horizontal(self,speed) + if finished then return true end + end + mobkit.queue_high(self,func,prty) +end + +function mobkit.hq_aqua_attack(self,prty,tgtobj,speed) + local tyaw = 0 + local prvscanpos = {x=0,y=0,z=0} + local init = true + local tgtbox = tgtobj:get_properties().collisionbox + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true end + if init then + mobkit.animate(self,'fast') + mobkit.make_sound(self,'attack') + init = false + end + local pos = mobkit.get_stand_pos(self) + local yaw = self.object:get_yaw() + local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) + if not vector.equals(prvscanpos,scanpos) then + prvscanpos=scanpos + local nyaw,height = aqua_radar_dumb(pos,yaw,speed*0.5) + if height and height > pos.y then + local vel = self.object:get_velocity() + vel.y = vel.y+1 + self.object:set_velocity(vel) + end + if yaw ~= nyaw then + tyaw=nyaw + mobkit.hq_aqua_turn(self,prty+1,tyaw,speed) + return + end + end + + local tpos = tgtobj:get_pos() + local tyaw=minetest.dir_to_yaw(vector.direction(pos,tpos)) + mobkit.turn2yaw(self,tyaw,3) + local yaw = self.object:get_yaw() + if mobkit.timer(self,1) then + if not mobkit.is_in_deep(tgtobj) then return true end + local vel = self.object:get_velocity() + if tpos.y>pos.y+0.5 then self.object:set_velocity({x=vel.x,y=vel.y+0.5,z=vel.z}) + elseif tpos.y bpos.x+box[1] and pos.x < bpos.x+box[4] and + pos.y > bpos.y+box[2] and pos.y < bpos.y+box[5] and + pos.z > bpos.z+box[3] and pos.z < bpos.z+box[6] +end + +-- call this instead if you want feet position. +--[[ +function mobkit.get_stand_pos(thing) -- thing can be luaentity or objectref. + if type(thing) == 'table' then + return mobkit.pos_shift(thing.object:get_pos(),{y=thing.collisionbox[2]+0.01}) + elseif type(thing) == 'userdata' then + local colbox = thing:get_properties().collisionbox + return mobkit.pos_shift(thing:get_pos(),{y=colbox[2]+0.01}) + end +end --]] + +function mobkit.get_stand_pos(thing) -- thing can be luaentity or objectref. + local pos = {} + local colbox = {} + if type(thing) == 'table' then + pos = thing.object:get_pos() + colbox = thing.object:get_properties().collisionbox + elseif type(thing) == 'userdata' then + pos = thing:get_pos() + colbox = thing:get_properties().collisionbox + else + return false + end + return mobkit.pos_shift(pos,{y=colbox[2]+0.01}), pos +end + +function mobkit.set_acceleration(thing,vec,limit) + limit = limit or 100 + if type(thing) == 'table' then thing=thing.object end + vec.x=mobkit.minmax(vec.x,limit) + vec.y=mobkit.minmax(vec.y,limit) + vec.z=mobkit.minmax(vec.z,limit) + + thing:set_acceleration(vec) +end + +function mobkit.nodeatpos(pos) + local node = minetest.get_node_or_nil(pos) + if node then return minetest.registered_nodes[node.name] end +end + +function mobkit.get_nodename_off(pos,vec) + return minetest.get_node(mobkit.pos_shift(pos,vec)).name +end + +function mobkit.get_node_pos(pos) + return { + x=floor(pos.x+0.5), + y=floor(pos.y+0.5), + z=floor(pos.z+0.5), + } +end + +function mobkit.get_nodes_in_area(pos1,pos2,full) + local npos1=mobkit.get_node_pos(pos1) + local npos2=mobkit.get_node_pos(pos2) + local result = {} + local cnt = 0 -- safety + + local sx = (pos2.x 125 then + minetest.chat_send_all('get_nodes_in_area: area too big ') + return result + end + + until y==npos2.y + until z==npos2.z + until x==npos2.x + + return result +end + +function mobkit.get_hitbox_bottom(self) + local y = self.collisionbox[2] + local pos = self.object:get_pos() + return { + {x=pos.x+self.collisionbox[1],y=pos.y+y,z=pos.z+self.collisionbox[3]}, + {x=pos.x+self.collisionbox[1],y=pos.y+y,z=pos.z+self.collisionbox[6]}, + {x=pos.x+self.collisionbox[4],y=pos.y+y,z=pos.z+self.collisionbox[3]}, + {x=pos.x+self.collisionbox[4],y=pos.y+y,z=pos.z+self.collisionbox[6]}, + } +end + +function mobkit.get_node_height(pos) + local npos = mobkit.get_node_pos(pos) + local node = mobkit.nodeatpos(npos) + if node == nil then return nil end + + if node.walkable then + if node.drawtype == 'nodebox' then + if node.node_box and node.node_box.type == 'fixed' then + if type(node.node_box.fixed[1]) == 'number' then + return npos.y + node.node_box.fixed[5] ,0, false + elseif type(node.node_box.fixed[1]) == 'table' then + return npos.y + node.node_box.fixed[1][5] ,0, false + else + return npos.y + 0.5,1, false -- todo handle table of boxes + end + elseif node.node_box and node.node_box.type == 'leveled' then + return minetest.get_node_level(pos)/64-0.5+mobkit.get_node_pos(pos).y, 0, false + else + return npos.y + 0.5,1, false -- the unforeseen + end + else + return npos.y+0.5,1, false -- full node + end + else + local liquidflag = false + if node.drawtype == 'liquid' then liquidflag = true end + return npos.y-0.5,-1,liquidflag + end +end + +-- get_terrain_height +-- steps(optional) number of recursion steps; default=3 +-- dir(optional) is 1=up, -1=down, 0=both; default=0 +-- liquidflag(forbidden) never provide this parameter. +function mobkit.get_terrain_height(pos,steps,dir,liquidflag) --dir is 1=up, -1=down, 0=both + steps = steps or 3 + dir = dir or 0 + + local h,f,l = mobkit.get_node_height(pos) + if h == nil then return nil end + if l then liquidflag = true end + + if f==0 then + return h, liquidflag + end + + if dir==0 or dir==f then + steps = steps - 1 + if steps <=0 then return nil end + return mobkit.get_terrain_height(mobkit.pos_shift(pos,{y=f}),steps,f,liquidflag) + else + return h, liquidflag + end +end + +function mobkit.get_spawn_pos_abr(dtime,intrvl,radius,chance,reduction) + dtime = min(dtime,0.1) + local plyrs = minetest.get_connected_players() + intrvl=1/intrvl + + if random() 1 then + -- spawn in the front arc + yaw = minetest.dir_to_yaw(vel) + random()*0.35 - 0.75 + else + -- random yaw + yaw = random()*pi*2 - pi + end + local pos = plyr:get_pos() + local dir = vector.multiply(minetest.yaw_to_dir(yaw),radius) + local pos2 = vector.add(pos,dir) + pos2.y=pos2.y-5 + local height, liquidflag = mobkit.get_terrain_height(pos2,32) + if height then + local objs = minetest.get_objects_inside_radius(pos,radius*1.1) + for _,obj in ipairs(objs) do -- count mobs in abrange + if not obj:is_player() then + local lua = obj:get_luaentity() + if lua and lua.name ~= '__builtin:item' then + chance=chance + (1-chance)*reduction -- chance reduced for every mob in range + end + end + end + if chance < random() then + pos2.y = height + objs = minetest.get_objects_inside_radius(pos2,radius*0.95) + for _,obj in ipairs(objs) do -- do not spawn if another player around + if obj:is_player() then return end + end + return pos2, liquidflag + end + end + end +end + +function mobkit.turn2yaw(self,tyaw,rate) + tyaw = tyaw or 0 --temp + rate = rate or 6 + local yaw = self.object:get_yaw() + yaw = yaw+pi + tyaw=(tyaw+pi)%(pi*2) + + local step=min(self.dtime*rate,abs(tyaw-yaw)%(pi*2)) + + local dir = abs(tyaw-yaw)>pi and -1 or 1 + dir = tyaw>yaw and dir*1 or dir * -1 + + local nyaw = (yaw+step*dir)%(pi*2) + self.object:set_yaw(nyaw-pi) + + if nyaw==tyaw then return true, nyaw-pi + else return false, nyaw-pi end +end + +function mobkit.dir_to_rot(v,rot) + rot = rot or {x=0,y=0,z=0} + return {x = (v.x==0 and v.y==0 and v.z==0) and rot.x or math.atan2(v.y,vector.length({x=v.x,y=0,z=v.z})), + y = (v.x==0 and v.z==0) and rot.y or minetest.dir_to_yaw(v), + z=rot.z} +end + +function mobkit.rot_to_dir(rot) -- keep rot within <-pi/2,pi/2> + local dir = minetest.yaw_to_dir(rot.y) + dir.y = dir.y+tan(rot.x)*vector.length(dir) + return vector.normalize(dir) +end + +function mobkit.isnear2d(p1,p2,thresh) + if abs(p2.x-p1.x) < thresh and abs(p2.z-p1.z) < thresh then + return true + else + return false + end +end + +-- object has reached the destination if dest is in the rear half plane. +function mobkit.is_there_yet2d(pos,dir,dest) -- obj positon; facing vector; destination position + + local c = -dir.x*pos.x-dir.z*pos.z -- the constant + + if dir.z > 0 then + return dest.z <= (-dir.x*dest.x - c)/dir.z -- line equation + elseif dir.z < 0 then + return dest.z >= (-dir.x*dest.x - c)/dir.z + elseif dir.x > 0 then + return dest.x <= (-dir.z*dest.z - c)/dir.x + elseif dir.x < 0 then + return dest.x >= (-dir.z*dest.z - c)/dir.x + else + return false + end + +end + +function mobkit.isnear3d(p1,p2,thresh) + if abs(p2.x-p1.x) < thresh and abs(p2.z-p1.z) < thresh and abs(p2.y-p1.y) < thresh then + return true + else + return false + end +end + +function mobkit.get_box_intersect_cols(pos,box) + local pmin = {x=floor(pos.x+box[1]+0.5),z=floor(pos.z+box[3]+0.5)} + local pmax = {x=floor(pos.x+box[4]+0.5),z=floor(pos.z+box[6]+0.5)} + + result= {} + for x=pmin.x,pmax.x do + for z=pmin.z,pmax.z do + table.insert(result,{x=x,z=z}) + end + end + return result +end + +function mobkit.get_box_displace_cols(pos,box,vec,dist) + + local result = {{}} + -- front facing corner pos and neighbors + local fpos = {pos.y} + local xpos={pos.y} + local zpos={pos.y} + local xoff=nil + local zoff=nil + + if vec.x < 0 then + fpos.x = pos.x+box[1] -- frontmost corner's x + xoff = box[4]-box[1] -- edge offset along x + else + fpos.x = pos.x+box[4] + xoff = box[1]-box[4] + end + + if vec.z < 0 then + fpos.z = pos.z+box[3] -- frontmost corner's z + zoff = box[6]-box[3] -- edge offset along z + else + fpos.z = pos.z+box[6] + zoff = box[3]-box[6] + end + + -- displacement vector + if dist then vec = vector.multiply(vector.normalize(vec),dist) end + + -- traverse x + local xsgn = sign(vec.x) + local zsgn = sign(zoff) + local index=0 + for x = floor(fpos.x+0.5)+xsgn*0.5, fpos.x+vec.x, xsgn do + index=index+1 + if index > 50 then return result end + result[index] = result[index] or {} + local zcomp = vec.x == 0 and 0 or fpos.z + (x-fpos.x)*vec.z/vec.x -- z component at the intersection of x and node edge + for z = floor(zcomp+0.5), floor(zcomp+zoff+0.5), zsgn do + table.insert(result[index],{x=x+xsgn*0.5,z=z}) + end + end + + -- traverse z + local zsgn = sign(vec.z) + local xsgn = sign(xoff) + index=0 + for z = floor(fpos.z + 0.5)+zsgn*0.5, fpos.z+vec.z, zsgn do + index=index+1 + if index > 50 then return result end + result[index] = result[index] or {} + local xcomp = vec.z == 0 and 0 or fpos.x + (z-fpos.z)*vec.x/vec.z + for x = floor(xcomp+0.5), floor(xcomp+xoff+0.5), xsgn do + table.insert(result[index],{x=x,z=z+zsgn*0.5}) + end + end + + return result +end + +function mobkit.get_box_height(thing) + if type(thing) == 'table' then thing = thing.object end + local colbox = thing:get_properties().collisionbox + local height + if colbox then height = colbox[5]-colbox[2] + else height = 0.1 end + + return height > 0 and height or 0.1 +end + +function mobkit.is_alive(thing) -- thing can be luaentity or objectref. +-- if not thing then return false end + if not mobkit.exists(thing) then return false end + if type(thing) == 'table' then return thing.hp > 0 end + if thing:is_player() then return thing:get_hp() > 0 + else + local lua = thing:get_luaentity() + local hp = lua and lua.hp or nil + return hp and hp > 0 + end +end + +function mobkit.exists(thing) + if not thing then return false end + if type(thing) == 'table' then thing=thing.object end + if type(thing) == 'userdata' then + if thing:is_player() then + if thing:get_look_horizontal() then return true end + else + if thing:get_yaw() then return true end + end + end +end + +function mobkit.hurt(luaent,dmg) + if not luaent then return false end + if type(luaent) == 'table' then + luaent.hp = max((luaent.hp or 0) - dmg,0) + end +end + +function mobkit.heal(luaent,dmg) + if not luaent then return false end + if type(luaent) == 'table' then + luaent.hp = min(luaent.max_hp,(luaent.hp or 0) + dmg) + end +end + +function mobkit.animate(self,anim) + if self.animation and self.animation[anim] then + if self._anim == anim then return end + self._anim=anim + + local aparms = {} + if #self.animation[anim] > 0 then + aparms = self.animation[anim][random(#self.animation[anim])] + else + aparms = self.animation[anim] + end + + aparms.frame_blend = aparms.frame_blend or 0 + + self.object:set_animation(aparms.range,aparms.speed,aparms.frame_blend,aparms.loop) + else + self._anim = nil + end +end + +function mobkit.make_sound(self, sound) + local spec = self.sounds and self.sounds[sound] + local param_table = {object=self.object} + + if type(spec) == 'table' then + --pick random sound if it's a spec for random sounds + if #spec > 0 then spec = spec[random(#spec)] end + + --returns value or a random value within the range [value[1], value[2]) + local function in_range(value) + return type(value) == 'table' and value[1]+random()*(value[2]-value[1]) or value + end + + --pick random values within a range if they're a table + param_table.gain = in_range(spec.gain) + param_table.fade = in_range(spec.fade) + param_table.pitch = in_range(spec.pitch) + return minetest.sound_play(spec.name, param_table) + end + return minetest.sound_play(spec, param_table) +end + +function mobkit.go_forward_horizontal(self,speed) -- sets velocity in yaw direction, y component unaffected + local y = self.object:get_velocity().y + local yaw = self.object:get_yaw() + local vel = vector.multiply(minetest.yaw_to_dir(yaw),speed) + vel.y = y + self.object:set_velocity(vel) +end + +function mobkit.drive_to_pos(self,tpos,speed,turn_rate,dist) + local pos=self.object:get_pos() + dist = dist or 0.2 + if mobkit.isnear2d(pos,tpos,dist) then return true end + local tyaw = minetest.dir_to_yaw(vector.direction(pos,tpos)) + mobkit.turn2yaw(self,tyaw,turn_rate) + mobkit.go_forward_horizontal(self,speed) + return false +end + +function mobkit.timer(self,s) -- returns true approx every s seconds + local t1 = floor(self.time_total) + local t2 = floor(self.time_total+self.dtime) + if t2>t1 and t2%s==0 then return true end +end + +-- Memory functions. +-- Stuff in memory is serialized, never try to remember objectrefs. +function mobkit.remember(self,key,val) + self.memory[key]=val + return val +end + +function mobkit.forget(self,key) + self.memory[key] = nil +end + +function mobkit.recall(self,key) + return self.memory[key] +end + +-- Queue functions +function mobkit.queue_high(self,func,priority) + local maxprty = mobkit.get_queue_priority(self) + if priority > maxprty then + mobkit.clear_queue_low(self) + end + + for i,f in ipairs(self.hqueue) do + if priority > f.prty then + table.insert(self.hqueue,i,{func=func,prty=priority}) + return + end + end + table.insert(self.hqueue,{func=func,prty=priority}) +end + +function mobkit.queue_low(self,func) + table.insert(self.lqueue,func) +end + +function mobkit.is_queue_empty_low(self) + if #self.lqueue == 0 then return true + else return false end +end + +function mobkit.clear_queue_high(self) + self.hqueue = {} +end + +function mobkit.clear_queue_low(self) + self.lqueue = {} +end + +function mobkit.get_queue_priority(self) + if #self.hqueue > 0 then + return self.hqueue[1].prty + else return 0 end +end + +function mobkit.is_queue_empty_high(self) + if #self.hqueue == 0 then return true + else return false end +end + +function mobkit.get_nearby_player(self) -- returns random player if nearby or nil + for _,obj in ipairs(self.nearby_objects) do + if obj:is_player() and mobkit.is_alive(obj) then return obj end + end + return +end + +function mobkit.get_nearby_entity(self,name) -- returns random nearby entity of name or nil + for _,obj in ipairs(self.nearby_objects) do + if mobkit.is_alive(obj) and not obj:is_player() and obj:get_luaentity().name == name then return obj end + end + return +end + +function mobkit.get_closest_entity(self,name) -- returns closest entity of name or nil + local cobj = nil + local dist = abr*64 + local pos = self.object:get_pos() + for _,obj in ipairs(self.nearby_objects) do + local luaent = obj:get_luaentity() + if mobkit.is_alive(obj) and not obj:is_player() and luaent and luaent.name == name then + local opos = obj:get_pos() + local odist = abs(opos.x-pos.x) + abs(opos.z-pos.z) + if odist < dist then + dist=odist + cobj=obj + end + end + end + return cobj +end + +local function execute_queues(self) + --Execute hqueue + if #self.hqueue > 0 then + local func = self.hqueue[1].func + if func(self) then + table.remove(self.hqueue,1) + self.lqueue = {} + end + end + -- Execute lqueue + if #self.lqueue > 0 then + local func = self.lqueue[1] + if func(self) then + table.remove(self.lqueue,1) + end + end +end + +local function sensors() + local timer = 2 + local pulse = 1 + return function(self) + timer=timer-self.dtime + if timer < 0 then + + pulse = pulse + 1 -- do full range every third scan + local range = self.view_range + if pulse > 2 then + pulse = 1 + else + range = self.view_range*0.5 + end + + local pos = self.object:get_pos() +--local tim = minetest.get_us_time() + self.nearby_objects = minetest.get_objects_inside_radius(pos, range) +--minetest.chat_send_all(minetest.get_us_time()-tim) + for i,obj in ipairs(self.nearby_objects) do + if obj == self.object then + table.remove(self.nearby_objects,i) + break + end + end + timer=2 + end + end +end + +------------ +-- CALLBACKS +------------ + +function mobkit.default_brain(self) + if mobkit.is_queue_empty_high(self) then mobkit.hq_roam(self,0) end +end + +function mobkit.physics(self) + local vel=self.object:get_velocity() + local vnew = vector.new(vel) + -- dumb friction + + if self.isonground and not self.isinliquid then + vnew = {x= vel.x> 0.2 and vel.x*mobkit.friction or 0, + y=vel.y, + z=vel.z > 0.2 and vel.z*mobkit.friction or 0} + end + + -- bounciness + if self.springiness and self.springiness > 0 then + + if colinfo and colinfo.collides then + for _,c in ipairs(colinfo.collisions) do + if c.old_velocity[c.axis] > 0.1 then + vnew[c.axis] = vnew[c.axis] * self.springiness * -1 + end + end + elseif not colinfo then -- MT 5.2 and earlier + for _,k in ipairs({'y','z','x'}) do + if vel[k]==0 and abs(self.lastvelocity[k])> 0.1 then + vnew[k]=-self.lastvelocity[k]*self.springiness + end + end + end + end + + self.object:set_velocity(vnew) + + -- buoyancy + local surface = nil + local surfnodename = nil + local spos = mobkit.get_stand_pos(self) + spos.y = spos.y+0.01 + -- get surface height + local snodepos = mobkit.get_node_pos(spos) + local surfnode = mobkit.nodeatpos(spos) + while surfnode and surfnode.drawtype == 'liquid' do + surfnodename = surfnode.name + surface = snodepos.y+0.5 + if surface > spos.y+self.height then break end + snodepos.y = snodepos.y+1 + surfnode = mobkit.nodeatpos(snodepos) + end + self.isinliquid = surfnodename + if surface then -- standing in liquid +-- self.isinliquid = true + local submergence = min(surface-spos.y,self.height)/self.height +-- local balance = self.buoyancy*self.height + local buoyacc = mobkit.gravity*(self.buoyancy-submergence) + mobkit.set_acceleration(self.object, + {x=-vel.x*self.water_drag,y=buoyacc-vel.y*abs(vel.y)*0.4,z=-vel.z*self.water_drag}) + else +-- self.isinliquid = false + self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + end + +end + +function mobkit.vitals(self) + -- vitals: fall damage + local vel = self.object:get_velocity() + local velocity_delta = abs(self.lastvelocity.y - vel.y) + if velocity_delta > mobkit.safe_velocity then + self.hp = self.hp - floor(self.max_hp * min(1, velocity_delta/mobkit.terminal_velocity)) + end + + -- vitals: oxygen + if self.lung_capacity then + local colbox = self.object:get_properties().collisionbox + local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=colbox[5]})) -- node at hitbox top + if headnode and headnode.drawtype == 'liquid' then + self.oxygen = self.oxygen - self.dtime + else + self.oxygen = self.lung_capacity + end + + if self.oxygen <= 0 then self.hp=0 end -- drown + end +end + +function mobkit.statfunc(self) + local tmptab={} + tmptab.memory = self.memory + tmptab.hp = self.hp + tmptab.texture_no = self.texture_no + return minetest.serialize(tmptab) +end + +function mobkit.actfunc(self, staticdata, dtime_s) + + self.logic = self.logic or self.brainfunc + self.physics = self.physics or mobkit.physics + + self.lqueue = {} + self.hqueue = {} + self.nearby_objects = {} + self.nearby_players = {} + self.pos_history = {} + self.path_dir = 1 + self.time_total = 0 + self.water_drag = self.water_drag or 1 + + local sdata = minetest.deserialize(staticdata) + if sdata then + for k,v in pairs(sdata) do + self[k] = v + end + end + + if self.textures==nil then + local prop_tex = self.object:get_properties().textures + if prop_tex then self.textures=prop_tex end + end + + if not self.memory then -- this is the initial activation + self.memory = {} + + -- texture variation + if #self.textures > 1 then self.texture_no = random(#self.textures) end + end + + if self.timeout and ((self.timeout>0 and dtime_s > self.timeout and next(self.memory)==nil) or + (self.timeout<0 and dtime_s > abs(self.timeout))) then + self.object:remove() + end + + -- apply texture + if self.textures and self.texture_no then + local props = {} + props.textures = {self.textures[self.texture_no]} + self.object:set_properties(props) + end + +--hp + self.max_hp = self.max_hp or 10 + self.hp = self.hp or self.max_hp +--armor + if type(self.armor_groups) ~= 'table' then + self.armor_groups={} + end + self.armor_groups.immortal = 1 + self.object:set_armor_groups(self.armor_groups) + + self.buoyancy = self.buoyancy or 0 + self.oxygen = self.oxygen or self.lung_capacity + self.lastvelocity = {x=0,y=0,z=0} + self.sensefunc=sensors() +end + +function mobkit.stepfunc(self,dtime,colinfo) -- not intended to be modified + self.dtime = min(dtime,0.2) + self.colinfo = colinfo + self.height = mobkit.get_box_height(self) + +-- physics comes first + local vel = self.object:get_velocity() + + if colinfo then + self.isonground = colinfo.touching_ground + else + if self.lastvelocity.y==0 and vel.y==0 then + self.isonground = true + else + self.isonground = false + end + end + + self:physics() + + if self.logic then + if self.view_range then self:sensefunc() end + self:logic() + execute_queues(self) + end + + self.lastvelocity = self.object:get_velocity() + self.time_total=self.time_total+self.dtime +end + +-- load example behaviors +dofile(minetest.get_modpath("mobkit") .. "/example_behaviors.lua") + +minetest.register_on_mods_loaded(function() + local mbkfuns = '' + for n,f in pairs(mobkit) do + if type(f) == 'function' then + mbkfuns = mbkfuns .. n .. string.split(minetest.serialize(f),'.lua')[2] or '' + end + end + local crc = minetest.sha1(mbkfuns) +-- dbg(crc) +-- if crc ~= 'a061770008fe9ecf8e1042a227dc3beabd10e481' then +-- minetest.log("error","Mobkit namespace inconsistent, has been modified by other mods.") +-- end +end) diff --git a/mods/ENTITIES/mobkit/mobkit_api.txt b/mods/ENTITIES/mobkit/mobkit_api.txt new file mode 100644 index 000000000..599711e28 --- /dev/null +++ b/mods/ENTITIES/mobkit/mobkit_api.txt @@ -0,0 +1,560 @@ +Contents + +1 Concepts + 1.1 Behavior functions + 1.1.1 Low level functions + 1.1.2 High level functions + 1.1.2.1 Priority + 1.1.3 Modifying built in behaviors + 1.2 Logic function + 1.3 Processing diagram + 1.4 Entity definition + 1.5 Exposed luaentity members + +2 Reference + 2.1 Utility functions + 2.2 Built in behaviors + 2.2.1 High level behaviors + 2.2.2 Low level behaviors + 2.3 Constants and member variables + +----------- +1. Concepts +----------- + +1.1 Behavior functions + +These are the most fundamental units of code, every action entities can perform is a separate function. +There are two types of behaviors: +- low level, these govern physical actions and interactions (think moves) +- high level, these are logical structures governing low level behaviors in order to perform more complex tasks + +Behaviors run for considerable amount of time, this means the functions are being called repeatedly on consecutive engine steps. +Therefore a need for preserving state between calls, this is why they are implemented as closures, see defining conventions for details. + +Behavior functions are active until they finish the job, are removed from the queue or superseded by a higher priority behavior. +They signal finished state by returning true, therefore it's very important to carefully design the completion conditions + +For a behavior to begin executing it has to be put on a queue. There are two separate queues, one for low and one for high level behaviors. +Queuing is covered by behavour defining conventions + +Mobkit comes with some example behavior functions, which are located in /example_behaviors.lua +!!! In simplest scenarios there's no need to code behaviors, much can be achieved using only built-in stuff !!! +!!! To start using the api it's enough to learn defining mobs and writing brain functions !!! + + +1.1.1 Low level behavior functions + +These are physical actions and interactions: steps, jumps, turns etc. here you'll set velocity, yaw, kick off animations and sounds. + +Low level behavior definition: + +function mobkit.lq_bhv1(self,[optional additional persistent parameters]) -- enclosing function + ... -- optional definitions of additional persistent variables + local func=function(self) -- enclosed function, self is mandatory and the only allowed parameter + ... -- actual function definition, remember to return true eventually + end + mobkit.queue_low(self,func) -- this will queue the behavior at the time of lq_bhv1 call +end + + +1.1.2 High level behavior functions + +These are complex tasks like getting to a position, following other objects, hiding, patrolling an area etc. +Their job is tracking changes in the environment and managing low level behavior queue accordingly. + +High level behavior definition: + +function mobkit.hq_bhv1(self,priority,[optional additional persistent parameters]) -- enclosing function + ... -- optional definitions of additional persistent variables + local func=function(self) -- enclosed function, self is mandatory and the only allowed parameter + ... -- actual function definition, remember to return true eventually + end + mobkit.queue_high(self,func,priority) -- this will queue the behavior at the time of hq_bhv1 call +end + + +1.1.2.1 Priority + +Unlike low level behaviors which are executed in FIFO order, high level behaviors support prioritization. +This concept is essential for making sure the right behavior is active at the right time. +Prioritization is what makes it possible to interrupt a task in order to perform a more important one + +The currently executing behavior is always the first in the queue. +When a new behavior is placed onto the queue: +If the queue is not empty a new behavior is inserted before the first behavior of lower priority if such exists, or last. +If the new behavior supersedes the one currently executing, low level queue is purged immediately. + +Common idioms: + +hq_bhv1(self,prty): + ... + hq_bhv2(self,prty) -- bhv1 kicks off bhv2 with equal priority + return true -- and ends, + -- bhv2 becomes active on the next engine step. + +hq_bhv1(self,prty): + ... + hq_bhv2(self,prty+1) -- bhv1 kicks off bhv2 with higher priority + -- bhv2 takes over and when it ends, bhv1 resumes. + + +Particular prioritization scheme is to be designed by the user according to specific mod requirements. + +1.1.3 Modifying built in behaviors + +Do not modify example_behaviors.lua directly, because functions defined there are meant to be shared between mods. +Instead, copy the contents of /behaviors2override.lua into your mod/game, changing every occurence of the string '[yournamespace]' to the name of a lua table representing your namespace of choice. + +1.2 Logic function +------------------ +Every mob must have one. +Its job is managing high level behavior queue in response to events which are not intercepted by callbacks. +Contrary to what the name suggests, these functions needn't necessarily be too complex thanks to their limited responsibilities. + +Typical flow might look like this: + +if mobkit.timer(self,1) then -- returns true approx every second + local prty = mobkit.get_queue_priority(self) + + if prty < 20 + if ... then + hq_do_important_stuff(self,20) + return + end + end + + if prty < 10 then + if ... then + hq_do_something_else(self,10) + return + elseif ... then + hq_do_this_instead(self,10) + return + end + end + + if mobkit.is_queue_empty_high(self) then + hq_fool_around(self,0) + end +end + + +1.3 Processing diagram +---------------------- + + --------------------------------------- +| PHYSICS | +| | +| ----------------------- | +| | Logic Function | | +| ----------------------- | +| | | +| -----|----------------- | +| | V HL Queue| | +| | 1| 2| 3|... | | +| ----------------------- | +| | | +| -----|----------------- | +| | V LL Queue| | +| | 1| 2| 3|... | | +| ----------------------- | +| | + --------------------------------------- + + Order of execution during an engine step: + First comes physics: gravity, buoyancy, friction etc., then the logic function is called. + After that, the first behavior on the high level queue, if exists, + and the last, the first low level behavior if present. + +1.4 Entity definition +--------------------- + +minetest.register_entity("mod:name",{ + + -- required minetest api props + + initial_properties = { + physical = true, + collide_with_objects = true, + collisionbox = {...}, + visual = "mesh", + mesh = "...", + textures = {...}, + }, + + + -- required mobkit props + + timeout = [num], -- entities are removed after this many seconds inactive + -- 0 is never + -- mobs having memory entries are not affected + + buoyancy = [num], -- (0,1) - portion of collisionbox submerged + -- = 1 - controlled buoyancy (fish, submarine) + -- > 1 - drowns + -- < 0 - MC like water trampolining + + lung_capacity = [num], -- seconds + max_hp = [num], + on_step = mobkit.stepfunc, + on_activate = mobkit.actfunc, + get_staticdata = mobkit.statfunc, + logic = [function user defined], -- older 'brainfunc' name works as well. + + -- optional mobkit props + -- or used by built in behaviors + physics = [function user defined] -- optional, overrides built in physics + animation = { + [name]={range={x=[num],y=[num]},speed=[num],loop=[bool]}, -- single + + [name]={ -- variant, animations are chosen randomly. + {range={x=[num],y=[num]},speed=[num],loop=[bool]}, + {range={x=[num],y=[num]},speed=[num],loop=[bool]}, + ... + } + ... + } + sounds = { + [name] = [string filename], --single, simple, + + [name] = { --single, more powerful. All fields but 'name' are optional + name = [string filename], + gain=[num or range], --range is a table of the format {left_bound, right_bound} + fade=[num or range], + pitch=[num or range], + }, + + [name] = { --variant, sound is chosen randomly + { + name = [string filename], + gain=[num or range], + fade=[num or range], + pitch=[num or range], + }, + { + name = [string filename], + gain=[num or range], + fade=[num or range], + pitch=[num or range], + }, + ... + }, + ... + }, + max_speed = [num], -- m/s + jump_height = [num], -- nodes/meters + view_range = [num], -- nodes/meters + attack={range=[num], -- range is distance between attacker's collision box center + damage_groups={fleshy=[num]}}, -- and the tip of the murder weapon in nodes/meters + armor_groups = {fleshy=[num]} +}) + +1.5 Exposed luaentity members + +Some frequently used entity fields to be accessed directly for convenience + + self.dtime -- max(dtime as passed to on_step,0.5) - limit of 0.05 to prevent jerkines on long steps. + self.hp -- hitpoints + self.isonground -- true if in collision with negative Y + self.isinliquid -- true if the node at foot level is drawtype=='liquid' + +------------ +2. Reference +------------ + +2.1 Utility Functions + +function mobkit.minmax(v,m) + -- v,n: numbers + -- returns v trimmed to <-m,m> range + +function mobkit.get_terrain_height(pos,steps) + -- recursively search for walkable surface at pos. + -- steps (optional) is how far from pos it gives up, expressed in nodes, default 3 + -- Returns: + -- surface height at pos, or nil if not found + -- liquid flag: true if found surface is covered with liquid + +function mobkit.turn2yaw(self,tyaw,rate) + -- gradually turns towards yaw + -- self: luaentity + -- tyaw: target yaw in radians + -- rate: turn rate in rads/s + --returns: true if facing tyaw; current yaw + +function mobkit.timer(self,s) + -- returns true approx every s seconds + -- used to reduce execution of code that needn't necessarily be done on every engine step + +function mobkit.pos_shift(pos,vec) + -- convenience function + -- returns pos shifted by vec + -- vec needn't have all three components given, absent components are assumed zero. + -- e.g pos_shift(pos,{y=1}) is valid + +function mobkit.pos_translate2d(pos,yaw,dist) + -- returns pos translated in the yaw direction by dist + +function mobkit.get_stand_pos(thing) + -- returns object pos projected onto the bottom collisionbox face + -- thing can be luaentity or objectref. + +function mobkit.nodeatpos(pos) + -- convenience function + -- returns nodedef or nil if it's an ignore node + +function mobkit.get_node_pos(pos) + -- returns center of the node that pos is inside + +function mobkit.get_nodes_in_area(pos1,pos2,[full]) + -- in basic mode returns a table of unique nodes within area indexed by node + -- in full=true mode returns a table of nodes indexed by pos + -- works for up to 125 nodes. + +function mobkit.isnear2d(p1,p2,thresh) + -- returns true if pos p2 is within a square with center at pos p1 and radius thresh + -- y components are ignored + +function mobkit.is_there_yet2d(pos,dir,dest) -- obj positon; facing vector; destination position + -- returns true if a position dest is behind position pos according to facing vector dir + -- (checks if dest is in the rear half plane as defined by pos and dir) + -- y components are ignored + +function mobkit.isnear3d(p1,p2,thresh) + -- returns true if pos p2 is within a cube with center at pos p1 and radius thresh + +function mobkit.get_box_intersect_cols(pos,box) + -- returns an array of {x=,z=} columns that the box intersects with. + +function mobkit.get_box_displace_cols(pos,box,vec,dist) + -- returns an array of {x=,z=} columns that the box would pass by if moved by horizontal vector vec + -- if dist provided, vec gets normalized. + +function mobkit.dir_to_rot(v,rot) + -- converts a 3d vector v to rotation like in set_rotation() object method + -- rot (optional) is current object rotation + +function mobkit.rot_to_dir(rot) + -- converts minetest rotation vector (pitch,yaw,roll) to direction unit vector + +function mobkit.is_alive(thing) + -- non essential, checks if thing exists in the world and is alive + -- makes an assumption that luaentities are considered dead when their hp < 100 + -- thing can be luaentity or objectref. + -- used for stored luaentities and objectrefs + +function mobkit.exists(thing) + -- checks if thing exists in the world + -- thing can be luaentity or objectref. + -- used for stored luaentities and objectrefs + +function mobkit.hurt(luaent,dmg) + -- decrease luaent.hp by dmg + +function mobkit.heal(luaent,dmg) + -- increase luaent.hp by dmg + +function mobkit.get_spawn_pos_abr(dtime,intrvl,radius,chance,reduction) + -- returns a potential spawn position at random intervals + -- intrvl: avg spawn attempt interval for every player + -- radius: spawn distance in nodes, active_block_range*16 is recommended + -- chance: (0,1) chance to spawn a mob if there are no other objects in area + -- reduction: (0,1) spawn chance is reduced by this factor for every object in range. + --usage: + minetest.register_globalstep(function(dtime) + local spawnpos = mobkit.get_spawn_pos_abr(...) + if spawnpos then + ... -- mod/game specific logic + end + end) + +function mobkit.animate(self,anim) + -- makes an entity play an animation of name anim, or does nothing if not defined + -- anim is string, see entity definition + -- does nothing if the same animation is already running + +function mobkit.make_sound(self,sound) + -- sound is string, see entity definition + -- makes an entity play sound, or does nothing if not defined + --returns sound handle + +function mobkit.go_forward_horizontal(self,speed) + -- sets an entity's horizontal velocity in yaw direction. Vertical velocity unaffected. + +function mobkit.drive_to_pos(self,tpos,speed,turn_rate,dist) + -- moves in facing direction while gradually turning towards tpos, returns true if in dist distance from tpos + -- tpos: target position + -- speed: in m/s + -- turn_rate: in rad/s + -- dist: in m. + +-- Memory functions. + +This represents mob long term memory +Warning: Stuff in memory is serialized, never try to remember objectrefs or tables referencing them +or the engine will crash. + +function mobkit.remember(self,key,val) + -- premanently store a key, value pair +function mobkit.forget(self,key) + -- clears a memory entry +function mobkit.recall(self,key) + -- returns val associated with key + +-- Queue functions + +function mobkit.queue_high(self,func,priority) + -- only for use in behavior definitions, see 1.1.2 + +function mobkit.queue_low(self,func) + -- only for use in behavior definitions, see 1.1.1 + + +function mobkit.clear_queue_high(self) +function mobkit.clear_queue_low(self) + +function mobkit.is_queue_empty_high(self) +function mobkit.is_queue_empty_low(self) + +function mobkit.get_queue_priority(self) + -- returns the priority of currently running behavior + -- this is also the highest of all queued behaviors + + +-- Use these inside logic functions -- + +function mobkit.vitals(self) + -- default drowning and fall damage, call it before hp check +function mobkit.get_nearby_player(self) + -- returns random player if nearby or nil +function mobkit.get_nearby_entity(self,name) + -- returns random nearby entity of name or nil +function mobkit.get_closest_entity(self,name) + -- returns closest entity of name or nil + + +-- Misc + +Neighbors structure represents a node's horizontal neighbors +Not essential, used by some built in behaviors +Custom behaviors may not need it. + +Neighbor #1 is offset {x=1,z=0}, subsequent numbers go clockwise + +function mobkit.dir2neighbor(dir) + -- converts a 3d vector to neighbor number, y component ignored + +function mobkit.neighbor_shift(neighbor,shift) + -- get another neighbor number relative to the given, shift: plus is clockwise, minus the opposite + -- 1,1 = 2; 1,-2 = 7 + + +2.2 Built in behaviors + +function mobkit.goto_next_waypoint(self,tpos) + -- this functions groups common operations making mobs move in a specific direction + -- not a behavior itself, but is used by some built in HL behaviors + -- which use node by node movement algorithm + +2.2.1 High Level Behaviors -- + +function mobkit.hq_roam(self,prty) + -- slow random roaming + -- never returns + +function mobkit.hq_follow(self,prty,tgtobj) + -- follow the tgtobj + -- returns if tgtobj becomes inactive + +function mobkit.hq_goto(self,prty,tpos) + -- go to tpos position + -- returns on arrival + +function mobkit.hq_runfrom(self,prty,tgtobj) + -- run away from tgtobj object + -- returns when tgtobj far enough + +function mobkit.hq_hunt(self,prty,tgtobj) + -- follow tgtobj and when close enough, kick off hq_attack + -- returns when tgtobj too far + +function mobkit.hq_warn(self,prty,tgtobj) + -- when a tgtobj close by, turn towards them and make the 'warn' sound + -- kick off hq_hunt if tgtobj too close or timer expired + -- returns when tgtobj moves away + +function mobkit.hq_die(self) + -- default death, rotate and remove() after set time + +function mobkit.hq_attack(self,prty,tgtobj) + -- default attack, turns towards tgtobj and leaps + -- returns when tgtobj out of range + +function mobkit.hq_liquid_recovery(self,prty) + -- use when submerged in liquid, scan for nearest land + -- if land is found within view_range, kick off hq_swimto + -- otherwise die + +function mobkit.hq_swimto(self,prty,tpos) + -- swim towards the position tpos, jump if necessary + -- returns if standing firmly on dry land + + Aquatic behaviors: + + Macros: +function aqua_radar_dumb(pos,yaw,range,reverse) + -- assumes a mob will avoid shallows + -- checks if a pos in front of a moving entity swimmable + -- otherwise returns new position + +function mobkit.is_in_deep(target) + -- checks if an object is in water at least 2 nodes deep + + Hq Behaviors: +function mobkit.hq_aqua_roam(self,prty,speed) +function mobkit.hq_aqua_attack(self,prty,tgtobj,speed) +function mobkit.hq_aqua_turn(self,prty,tyaw,speed) + -- used by both previous bhv + +2.2.2 Low Level Behaviors -- + +function mobkit.lq_turn2pos(self,tpos) + -- gradually turn towards tpos position + -- returns when facing tpos + +function mobkit.lq_idle(self,duration) + -- do nothing for duration seconds + -- set 'stand' animation + +function mobkit.lq_dumbwalk(self,dest,speed_factor) + -- simply move towards dest + -- set 'walk' animation + +function mobkit.lq_dumbjump(self,height) + -- if standing on the ground, jump in the facing direction + -- height is relative to feet level + -- set 'stand' animation + +function mobkit.lq_freejump(self) + -- unconditional jump in the facing direction + -- useful e.g for getting out of water + -- returns when the apex has been reached + +function mobkit.lq_jumpattack(self,height,target) + -- jump towards the target, punch if a hit + -- returns after punch or on the ground + +function mobkit.lq_fallover(self) + -- gradually rotates Z = 0 to pi/2 + + +2.3 Constants and member variables -- + +mobkit.gravity = -9.8 +mobkit.friction = 0.4 -- inert entities will slow down when in contact with the ground + -- the smaller the number, the greater the effect + +self.dtime -- for convenience, dtime as passed to currently executing on_step() +self.isonground -- true if y velocity is 0 for at least two succesive steps +self.isinliquid -- true if feet submerged in liquid type=source diff --git a/mods/ENTITIES/mobkit/mod.conf b/mods/ENTITIES/mobkit/mod.conf new file mode 100644 index 000000000..d1b9d401b --- /dev/null +++ b/mods/ENTITIES/mobkit/mod.conf @@ -0,0 +1,2 @@ +name = mobkit +description = Entity API diff --git a/mods/ENTITIES/mobkit/screenshot.png b/mods/ENTITIES/mobkit/screenshot.png new file mode 100644 index 000000000..9f504203b Binary files /dev/null and b/mods/ENTITIES/mobkit/screenshot.png differ diff --git a/mods/ENTITIES/wildlife/LICENSE b/mods/ENTITIES/wildlife/LICENSE new file mode 100644 index 000000000..664fc8320 --- /dev/null +++ b/mods/ENTITIES/wildlife/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 TheTermos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mods/ENTITIES/wildlife/depends.txt b/mods/ENTITIES/wildlife/depends.txt new file mode 100644 index 000000000..2b0aae56c --- /dev/null +++ b/mods/ENTITIES/wildlife/depends.txt @@ -0,0 +1 @@ +mobkit \ No newline at end of file diff --git a/mods/ENTITIES/wildlife/init.lua b/mods/ENTITIES/wildlife/init.lua new file mode 100644 index 000000000..9c22b3158 --- /dev/null +++ b/mods/ENTITIES/wildlife/init.lua @@ -0,0 +1,303 @@ +local abr = minetest.get_mapgen_setting('active_block_range') + +local wildlife = {} +--wildlife.spawn_rate = 0.5 -- less is more + +local min=math.min +local max=math.max + +local spawn_rate = 1 - max(min(minetest.settings:get('wildlife_spawn_chance') or 0.2,1),0) +local spawn_reduction = minetest.settings:get('wildlife_spawn_reduction') or 0.5 + +local function node_dps_dmg(self) + local pos = self.object:get_pos() + local box = self.object:get_properties().collisionbox + local pos1 = {x = pos.x + box[1], y = pos.y + box[2], z = pos.z + box[3]} + local pos2 = {x = pos.x + box[4], y = pos.y + box[5], z = pos.z + box[6]} + local nodes_overlap = mobkit.get_nodes_in_area(pos1, pos2) + local total_damage = 0 + + for node_def, _ in pairs(nodes_overlap) do + local dps = node_def.damage_per_second + if dps then + total_damage = math.max(total_damage, dps) + end + end + + if total_damage ~= 0 then + mobkit.hurt(self, total_damage) + end +end + +local function predator_brain(self) + -- vitals should be checked every step + if mobkit.timer(self,1) then node_dps_dmg(self) end + mobkit.vitals(self) +-- if self.object:get_hp() <=100 then + if self.hp <= 0 then + mobkit.clear_queue_high(self) -- cease all activity + mobkit.hq_die(self) -- kick the bucket + return + end + + if mobkit.timer(self,1) then -- decision making needn't happen every engine step + local prty = mobkit.get_queue_priority(self) + + if prty < 20 and self.isinliquid then + mobkit.hq_liquid_recovery(self,20) + return + end + + local pos=self.object:get_pos() + + -- hunt + if prty < 10 then -- if not busy with anything important + local prey = mobkit.get_closest_entity(self,'wildlife:deer') -- look for prey + if prey then + mobkit.hq_hunt(self,10,prey) -- and chase it + end + end + + if prty < 9 then + local plyr = mobkit.get_nearby_player(self) + if plyr and vector.distance(pos,plyr:get_pos()) < 10 then -- if player close + mobkit.hq_warn(self,9,plyr) -- try to repel them + end -- hq_warn will trigger subsequent bhaviors if needed + end + + -- fool around + if mobkit.is_queue_empty_high(self) then + mobkit.hq_roam(self,0) + end + end +end + +local function herbivore_brain(self) + if mobkit.timer(self,1) then node_dps_dmg(self) end + mobkit.vitals(self) + + if self.hp <= 0 then + mobkit.clear_queue_high(self) + mobkit.hq_die(self) + return + end + + if mobkit.timer(self,1) then + local prty = mobkit.get_queue_priority(self) + + if prty < 20 and self.isinliquid then + mobkit.hq_liquid_recovery(self,20) + return + end + + local pos = self.object:get_pos() + + if prty < 11 then + local pred = mobkit.get_closest_entity(self,'wildlife:wolf') + if pred then + mobkit.hq_runfrom(self,11,pred) + return + end + end + if prty < 10 then + local plyr = mobkit.get_nearby_player(self) + if plyr and vector.distance(pos,plyr:get_pos()) < 8 then + mobkit.hq_runfrom(self,10,plyr) + return + end + end + if mobkit.is_queue_empty_high(self) then + mobkit.hq_roam(self,0) + end + end +end + +-- spawning is too specific to be included in the api, this is an example. +-- a modder will want to refer to specific names according to games/mods they're using +-- in order for mobs not to spawn on treetops, certain biomes etc. + +local function spawnstep(dtime) + + for _,plyr in ipairs(minetest.get_connected_players()) do + if math.random() 1 then + -- spawn in the front arc + yaw = plyr:get_look_horizontal() + math.random()*0.35 - 0.75 + else + -- random yaw + yaw = math.random()*math.pi*2 - math.pi + end + local pos = plyr:get_pos() + local dir = vector.multiply(minetest.yaw_to_dir(yaw),abr*16) + local pos2 = vector.add(pos,dir) + pos2.y=pos2.y-5 + local height, liquidflag = mobkit.get_terrain_height(pos2,32) + + if height and height >= 0 and not liquidflag -- and math.abs(height-pos2.y) <= 30 testin + and mobkit.nodeatpos({x=pos2.x,y=height-0.01,z=pos2.z}).is_ground_content then + + local objs = minetest.get_objects_inside_radius(pos,abr*16+5) + local wcnt=0 + local dcnt=0 + for _,obj in ipairs(objs) do -- count mobs in abrange + if not obj:is_player() then + local luaent = obj:get_luaentity() + if luaent and luaent.name:find('wildlife:') then + chance=chance + (1-chance)*spawn_reduction -- chance reduced for every mob in range + if luaent.name == 'wildlife:wolf' then wcnt=wcnt+1 + elseif luaent.name=='wildlife:deer' then dcnt=dcnt+1 end + end + end + end +--minetest.chat_send_all('chance '.. chance) + if chance < math.random() then + + -- if no wolves and at least one deer spawn wolf, else deer +-- local mobname = (wcnt==0 and dcnt > 0) and 'wildlife:wolf' or 'wildlife:deer' + local mobname = dcnt>wcnt+1 and 'wildlife:wolf' or 'wildlife:deer' + + pos2.y = height+0.5 + objs = minetest.get_objects_inside_radius(pos2,abr*16-2) + for _,obj in ipairs(objs) do -- do not spawn if another player around + if obj:is_player() then return end + end +--minetest.chat_send_all('spawnin '.. mobname ..' #deer:' .. dcnt) + minetest.add_entity(pos2,mobname) -- ok spawn it already damnit + end + end + end + end +end + + +minetest.register_globalstep(spawnstep) + +minetest.register_entity("wildlife:wolf",{ + -- common props + physical = true, + stepheight = 0.1, --EVIL! + collide_with_objects = true, + collisionbox = {-0.3, -0.00, -0.3, 0.3, 0.85, 0.3}, + visual = "mesh", + mesh = "mobs_mc_wolf.b3d", + textures = {"mobs_mc_wolf.png"}, + visual_size = {x = 1.3, y = 1.3}, + static_save = true, + makes_footstep_sound = true, + on_step = mobkit.stepfunc, -- required + on_activate = mobkit.actfunc, -- required + get_staticdata = mobkit.statfunc, + -- api props + springiness=0, + buoyancy = 0.75, -- portion of hitbox submerged + max_speed = 5, + jump_height = 1.26, + view_range = 24, + lung_capacity = 10, -- seconds + max_hp = 14, + timeout=600, + attack={range=0.5,damage_groups={fleshy=7}}, + sounds = { + attack = "mobs_mc_wolf_bark", + war_cry = "mobs_mc_wolf_growl", + warn = "mobs_mc_wolf_growl", + damage = {name = "mobs_mc_wolf_hurt", gain=0.6}, + death = {name = "mobs_mc_wolf_death", gain=0.6}, + eat = "mobs_mc_animal_eat_generic", + distance = 16, + }, + animation = { + walk={range={x=10,y=29},speed=30,loop=true}, + stand={range={x=1,y=5},speed=1,loop=true}, + }, + + brainfunc = predator_brain, + + on_punch=function(self, puncher, time_from_last_punch, tool_capabilities, dir) + if mobkit.is_alive(self) then + local hvel = vector.multiply(vector.normalize({x=dir.x,y=0,z=dir.z}),4) + self.object:set_velocity({x=hvel.x,y=2,z=hvel.z}) + + mobkit.hurt(self,tool_capabilities.damage_groups.fleshy or 1) + + if type(puncher)=='userdata' and puncher:is_player() then -- if hit by a player + mobkit.clear_queue_high(self) -- abandon whatever they've been doing + mobkit.hq_hunt(self,10,puncher) -- get revenge + end + end + end + +}) + +minetest.register_entity("wildlife:deer",{ + -- common props + physical = true, + stepheight = 0.1, --EVIL! + collide_with_objects = true, + hp_min = 10, + hp_max = 10, + xp_min = 1, + xp_max = 3, + rotate = 270, + collisionbox = {-0.45, -0.01, -0.45, 0.45, 1.39, 0.45}, + visual = "mesh", + mesh = "mobs_mc_cow.b3d", + textures = { + "mobs_mc_cow.png", + }, + visual_size = {x=2.8, y=2.8}, + static_save = true, + makes_footstep_sound = true, + on_step = mobkit.stepfunc, -- required + on_activate = mobkit.actfunc, -- required + get_staticdata = mobkit.statfunc, + -- api props + springiness=0, + buoyancy = 0.9, + max_speed = 5, + jump_height = 1.26, + view_range = 24, + lung_capacity = 10, -- seconds + max_hp = 10, + timeout = 600, + attack={range=0.5,damage_groups={fleshy=3}}, + sounds = { + random = "mobs_mc_cow", + scared = "mobs_mc_cow_hurt", + hurt = "mobs_mc_cow_hurt", + eat = "mobs_mc_animal_eat_generic", + distance = 16, + }, + animation = { + walk={range={x=10,y=29},speed=30,loop=true}, + stand={range={x=1,y=5},speed=1,loop=true}, + }, + + brainfunc = herbivore_brain, + + on_punch=function(self, puncher, time_from_last_punch, tool_capabilities, dir) + local hvel = vector.multiply(vector.normalize({x=dir.x,y=0,z=dir.z}),4) + self.object:set_velocity({x=hvel.x,y=2,z=hvel.z}) + mobkit.make_sound(self,'hurt') + mobkit.hurt(self,tool_capabilities.damage_groups.fleshy or 1) + end, +}) + + +-- minetest.register_on_chat_message( + -- function(name, message) + -- if message == 'doit' then + -- local plyr=minetest.get_player_by_name(name) + -- local pos=mobkit.get_stand_pos(plyr) + -- local nodes = mobkit.get_nodes_in_area(pos,mobkit.pos_shift(pos,{x=-1,z=-1,y=-1})) + -- for p,n in pairs(nodes) do + -- minetest.chat_send_all(p.name ..' '.. dump(n)) + -- end + -- end + -- end +-- ) diff --git a/mods/ENTITIES/wildlife/mod.conf b/mods/ENTITIES/wildlife/mod.conf new file mode 100644 index 000000000..46e1f7d98 --- /dev/null +++ b/mods/ENTITIES/wildlife/mod.conf @@ -0,0 +1,2 @@ +name = wildlife +depends = mobkit diff --git a/mods/ENTITIES/wildlife/models/herbivore.b3d b/mods/ENTITIES/wildlife/models/herbivore.b3d new file mode 100644 index 000000000..764735e23 Binary files /dev/null and b/mods/ENTITIES/wildlife/models/herbivore.b3d differ diff --git a/mods/ENTITIES/wildlife/models/wolf.b3d b/mods/ENTITIES/wildlife/models/wolf.b3d new file mode 100644 index 000000000..4f4a65dd6 Binary files /dev/null and b/mods/ENTITIES/wildlife/models/wolf.b3d differ diff --git a/mods/ENTITIES/wildlife/screenshot.png b/mods/ENTITIES/wildlife/screenshot.png new file mode 100644 index 000000000..58756969b Binary files /dev/null and b/mods/ENTITIES/wildlife/screenshot.png differ diff --git a/mods/ENTITIES/wildlife/settingtypes.txt b/mods/ENTITIES/wildlife/settingtypes.txt new file mode 100644 index 000000000..688725722 --- /dev/null +++ b/mods/ENTITIES/wildlife/settingtypes.txt @@ -0,0 +1,15 @@ +# This file contains settings of wildlife that can be changed in +# minetest.conf + +# Chance of spawning a mob when there are no other +# mobs in active_block_range around player +# must be float from 0 to 1 +# 1 is always, 0 is never +wildlife_spawn_chance (Spawn chance) float 0.3 + +# Base spawn chance is reduced by this factor +# for each mob within active_block_range +# must be float from 0 to 1 +# greater number is greater reduction +wildlife_spawn_reduction (Spawn chance reduction) float 0.2 + diff --git a/mods/ENTITIES/wildlife/sounds/Dog Bite-SoundBible.com-107030898.wav b/mods/ENTITIES/wildlife/sounds/Dog Bite-SoundBible.com-107030898.wav new file mode 100644 index 000000000..517c85ebb Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/Dog Bite-SoundBible.com-107030898.wav differ diff --git a/mods/ENTITIES/wildlife/sounds/Large Angry Dog 2-SoundBible.com-410353863.wav b/mods/ENTITIES/wildlife/sounds/Large Angry Dog 2-SoundBible.com-410353863.wav new file mode 100644 index 000000000..8b1a08fa1 Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/Large Angry Dog 2-SoundBible.com-410353863.wav differ diff --git a/mods/ENTITIES/wildlife/sounds/angrydog.ogg b/mods/ENTITIES/wildlife/sounds/angrydog.ogg new file mode 100644 index 000000000..2eb313863 Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/angrydog.ogg differ diff --git a/mods/ENTITIES/wildlife/sounds/deer_hurt.ogg b/mods/ENTITIES/wildlife/sounds/deer_hurt.ogg new file mode 100644 index 000000000..74f1a261a Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/deer_hurt.ogg differ diff --git a/mods/ENTITIES/wildlife/sounds/deer_hurt.wav b/mods/ENTITIES/wildlife/sounds/deer_hurt.wav new file mode 100644 index 000000000..095b3effc Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/deer_hurt.wav differ diff --git a/mods/ENTITIES/wildlife/sounds/deer_scared.ogg b/mods/ENTITIES/wildlife/sounds/deer_scared.ogg new file mode 100644 index 000000000..ceca6c9e3 Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/deer_scared.ogg differ diff --git a/mods/ENTITIES/wildlife/sounds/deer_scared.wav b/mods/ENTITIES/wildlife/sounds/deer_scared.wav new file mode 100644 index 000000000..6f4e4afd0 Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/deer_scared.wav differ diff --git a/mods/ENTITIES/wildlife/sounds/dogbite.ogg b/mods/ENTITIES/wildlife/sounds/dogbite.ogg new file mode 100644 index 000000000..391c31682 Binary files /dev/null and b/mods/ENTITIES/wildlife/sounds/dogbite.ogg differ diff --git a/mods/ENTITIES/wildlife/sounds/readme.txt b/mods/ENTITIES/wildlife/sounds/readme.txt new file mode 100644 index 000000000..119d8ca71 --- /dev/null +++ b/mods/ENTITIES/wildlife/sounds/readme.txt @@ -0,0 +1,2 @@ +dog sounds by Mike Koenig +http://soundbible.com \ No newline at end of file diff --git a/mods/ENTITIES/wildlife/textures/herbivore.png b/mods/ENTITIES/wildlife/textures/herbivore.png new file mode 100644 index 000000000..683bbc211 Binary files /dev/null and b/mods/ENTITIES/wildlife/textures/herbivore.png differ diff --git a/mods/ENTITIES/wildlife/textures/kit_wolf.png b/mods/ENTITIES/wildlife/textures/kit_wolf.png new file mode 100644 index 000000000..0db49dc5c Binary files /dev/null and b/mods/ENTITIES/wildlife/textures/kit_wolf.png differ diff --git a/mods/ENTITIES/wildlife/textures/kit_wolf.xcf b/mods/ENTITIES/wildlife/textures/kit_wolf.xcf new file mode 100644 index 000000000..6068f01af Binary files /dev/null and b/mods/ENTITIES/wildlife/textures/kit_wolf.xcf differ