From 04ae1483e77f6352b68260595c1c00c3ab0eec38 Mon Sep 17 00:00:00 2001 From: TheTermos <55103816+TheTermos@users.noreply.github.com> Date: Sat, 14 Sep 2019 17:26:56 +0200 Subject: [PATCH] Add files via upload --- behaviors.lua | 570 +++++++++++++++++++++ core.lua | 227 +++++++++ init.lua | 1331 ++++++++++++++++++++++++++++++++++++++++++++++++ mobkit_api.txt | 477 +++++++++++++++++ utility.lua | 1305 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 3910 insertions(+) create mode 100644 behaviors.lua create mode 100644 core.lua create mode 100644 init.lua create mode 100644 mobkit_api.txt create mode 100644 utility.lua diff --git a/behaviors.lua b/behaviors.lua new file mode 100644 index 0000000..f5bce03 --- /dev/null +++ b/behaviors.lua @@ -0,0 +1,570 @@ +-- node by node land movement macros +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) +--minetest.chat_send_all('pos2 ' .. minetest.serialize(pos2)) +--minetest.chat_send_all('nogopos ' .. minetest.serialize(nogopos)) + 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, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,4)) + 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 + -- 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 + +function mobkit.dumbstep(self,height,tpos,speed_factor) + 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 + mobkit.lq_idle(self,random(1,6)) +end + +---------------------------- +-- 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,yaw) + 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 phase=1 + local func=function(self) + if not mobkit.is_alive(target) then return true end + if self.isonground then + if phase==1 then -- collision bug workaround + local vel = self.object:get_velocity() + vel.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) + self.object:set_velocity(vel) + mobkit.make_sound(self,'charge') + phase=2 + else + return true + end + elseif phase==2 then + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + local vy = self.object:get_velocity().y + dir=vector.multiply(dir,6) + dir.y=vy + self.object:set_velocity(dir) + phase=3 + elseif phase==3 then -- in air + local twidth = target:get_properties().collisionbox[1] + local pos = self.object:get_pos() + -- calculate attack spot + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir2 = vector.add(dir,self.attack.range+twidth) + local apos = vector.add(pos,dir2) +-- local tpos = mobkit.get_stand_pos(target) --test +-- tpos.y = tpos.y+height + if mobkit.isnear2d(apos,target:get_pos(),0.25) 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') + phase=4 + 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.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) + timer=0 + tgttime = 0 + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true 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.brainfunc = 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.8 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 func = function(self) +-- if not self.isinliquid and mobkit.is_queue_empty_low(self) then return true end + if not self.isinliquid and self.isonground then return true end +-- local pos = self.object:get_pos() + local pos = mobkit.get_stand_pos(self) + local y=self.object:get_velocity().y + local pos2d = {x=pos.x,y=0,z=pos.z} + local dir=vector.normalize(vector.direction(pos2d,tpos)) + local yaw = minetest.dir_to_yaw(dir) + + if mobkit.timer(self,1) then +--perpendicular vectors: {-z,x};{z,-x} + local offset=self.collisionbox[1] + local pos1 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=-dir.z*offset,z=dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos1) + if h and h>pos.y then + mobkit.lq_freejump(self) + else + local pos2 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=dir.z*offset,z=-dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos2) + if h and h>pos.y then + mobkit.lq_freejump(self) + 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 diff --git a/core.lua b/core.lua new file mode 100644 index 0000000..4637f8f --- /dev/null +++ b/core.lua @@ -0,0 +1,227 @@ + +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.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.lqueue = {} + self.hqueue = {} + self.nearby_objects = {} + self.nearby_players = {} + self.pos_history = {} + self.path_dir = 1 + self.time_total = 0 + + local sdata = minetest.deserialize(staticdata) + if sdata then + for k,v in pairs(sdata) do + self[k] = v + end + end + + if self.timeout and self.timeout>0 and dtime_s > self.timeout and next(self.memory)==nil then + self.object:remove() + 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 + + -- apply texture + if self.texture_no then + local props = {} + props.textures = {self.textures[self.texture_no]} + self.object:set_properties(props) + end + +--hp + self.hp = self.hp or (self.max_hp or 10) +--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.oxygen = self.oxygen or self.lung_capacity + self.lastvelocity = {x=0,y=0,z=0} + self.height = self.collisionbox[5] - self.collisionbox[2] + self.sensefunc=sensors() +end + +function mobkit.stepfunc(self,dtime) -- not intended to be modified + self.dtime = dtime +-- physics comes first +-- self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + local vel = self.object:get_velocity() + +-- if self.lastvelocity.y == vel.y then + if abs(self.lastvelocity.y-vel.y)<0.001 then + self.isonground = true + else + self.isonground = false + end + + -- dumb friction + if self.isonground then + self.object:set_velocity({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 + local vnew = vector.new(vel) + + if not self.collided then -- ugly workaround for inconsistent collisions + 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 + + if not vector.equals(vel,vnew) then + self.collided = true + else + if self.collided then + vnew = vector.new(self.lastvelocity) + end + self.collided = false + end + + self.object:set_velocity(vnew) + end + + -- buoyancy + local spos = mobkit.get_stand_pos(self) + spos.y = spos.y+0.01 + -- get surface height +-- local surface = mobkit.get_node_pos(spos).y+0.5 + local surface = nil + local snodepos = mobkit.get_node_pos(spos) + local surfnode = mobkit.nodeatpos(spos) + while surfnode and surfnode.drawtype == 'liquid' do + surface = snodepos.y+0.5 + if surface > spos.y+self.height then break end + snodepos.y = snodepos.y+1 + surfnode = mobkit.nodeatpos(snodepos) + end + if surface then -- standing in liquid + self.isinliquid = true + local submergence = min(surface-spos.y,self.height) + local balance = self.buoyancy*self.height + local buoyacc = mobkit.gravity*((balance - submergence)^2/balance^2*sign(balance - submergence)) + self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.7,z=-vel.z}) + else + self.isinliquid = false + self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + end + + + + -- local footnode = mobkit.nodeatpos(spos) + -- local headnode + -- if footnode and footnode.drawtype == 'liquid' then + + -- vel = self.object:get_velocity() + -- headnode = mobkit.nodeatpos(mobkit.pos_shift(spos,{y=self.height or 0})) -- TODO: height may be nil + -- local submergence = headnode.drawtype=='liquid' + -- and self.buoyancy-1 + -- or (self.buoyancy*self.height-(1-(spos.y+0.5)%1))^2/(self.buoyancy*self.height)^2*sign(self.buoyancy*self.height-(1-(spos.y+0.5)%1)) + + -- local buoyacc = submergence * mobkit.gravity + -- self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.5,z=-vel.z}) + + -- end + + if self.brainfunc then + -- vitals: fall damage + 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-100) * min(1, velocity_delta/mobkit.terminal_velocity)) + end + + -- vitals: oxygen + local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=self.collisionbox[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 + + + self:sensefunc() + self:brainfunc() + execute_queues(self) + end + + self.lastvelocity = self.object:get_velocity() + self.time_total=self.time_total+self.dtime +end \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..d6934b4 --- /dev/null +++ b/init.lua @@ -0,0 +1,1331 @@ +-- yaw values: +-- x+ = -pi/2 +-- x- = +pi/2 +-- z+ = 0 +-- z- = -pi + +mobkit={} + +mobkit.gravity = -9.8 +mobkit.friction = 0.4 -- less is more + +local abs = math.abs +local pi = math.pi +local floor = math.floor +local random = math.random +local sqrt = math.sqrt +local max = math.max +local min = math.min +local pow = math.pow + +local sign = function(x) + return (x<0) and -1 or 1 +end + +mobkit.terminal_velocity = sqrt(2*-mobkit.gravity*20) -- 20 meter fall = dead +mobkit.safe_velocity = sqrt(2*-mobkit.gravity*5) -- 5 m safe fall + +local abr = minetest.get_mapgen_setting('active_block_range') + +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} + } + +local modpath = minetest.get_modpath("mobkit") +dofile(modpath .. "/utility.lua") +dofile(modpath .. "/core.lua") +dofile(modpath .. "/behaviors.lua") + + +-- UTILITY FUNCTIONS + +function mobkit.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 mobkit.neighbor_shift(neighbor,shift) -- int shift: minus is left, plus is right + return (8+neighbor+shift-1)%8+1 +end + +function mobkit.pos_shift(pos,vec) + vec.x=vec.x or 0 + vec.y=vec.y or 0 + vec.z=vec.z or 0 + return {x=pos.x+vec.x, + y=pos.y+vec.y, + z=pos.z+vec.z} +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.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.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.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) + 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() + + local diff = tyaw-yaw + local step = self.dtime*rate + + if abs(diff) pi and -1 or 1 + + local nyaw = diff < 0 and yaw+step*-dirmod or yaw+step*dirmod + + if nyaw > pi then + nyaw=nyaw-pi*2 + elseif nyaw < -pi then + nyaw=nyaw+pi*2 + end + self.object:set_yaw(nyaw) +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.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.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 = (luaent.hp or 0) - dmg + 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 + -- local crange = self.object:get_animation().range + -- if not crange + -- or crange.x ~= self.animation[anim].range.x + -- or crange.y ~= self.animation[anim].range.y then + -- self.object:set_animation(self.animation[anim].range,self.animation[anim].speed,0,self.animation[anim].loop) + -- end + -- 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 + + self.object:set_animation(aparms.range,aparms.speed,0,aparms.loop) + else + self._anim = nil + end +end + +function mobkit.make_sound(self,sound) + if self.sounds and self.sounds[sound] then + minetest.sound_play(self.sounds[sound], {object=self.object}) + end +end + +function mobkit.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 height, liquidflag = mobkit.get_terrain_height(tpos) + + 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) + 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) + 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) +--minetest.chat_send_all('pos2 ' .. minetest.serialize(pos2)) +--minetest.chat_send_all('nogopos ' .. minetest.serialize(nogopos)) + 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, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,4)) + 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 + -- 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 + +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 +---------------------------- +-- 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,yaw) + 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 phase=1 + local func=function(self) + if not mobkit.is_alive(target) then return true end + if self.isonground then + if phase==1 then -- collision bug workaround + local vel = self.object:get_velocity() + vel.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) + self.object:set_velocity(vel) + mobkit.make_sound(self,'charge') + phase=2 + else + return true + end + elseif phase==2 then + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + local vy = self.object:get_velocity().y + dir=vector.multiply(dir,6) + dir.y=vy + self.object:set_velocity(dir) + phase=3 + elseif phase==3 then -- in air + local twidth = target:get_properties().collisionbox[1] + local pos = self.object:get_pos() + -- calculate attack spot + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir2 = vector.add(dir,self.attack.range+twidth) + local apos = vector.add(pos,dir2) +-- local tpos = mobkit.get_stand_pos(target) --test +-- tpos.y = tpos.y+height + if mobkit.isnear2d(apos,target:get_pos(),0.25) 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') + phase=4 + 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) + 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 + mobkit.lq_idle(self,random(1,6)) +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) + timer=0 + tgttime = 0 + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true 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.brainfunc = 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.8 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 func = function(self) +-- if not self.isinliquid and mobkit.is_queue_empty_low(self) then return true end + if not self.isinliquid and self.isonground then return true end +-- local pos = self.object:get_pos() + local pos = mobkit.get_stand_pos(self) + local y=self.object:get_velocity().y + local pos2d = {x=pos.x,y=0,z=pos.z} + local dir=vector.normalize(vector.direction(pos2d,tpos)) + local yaw = minetest.dir_to_yaw(dir) + + if mobkit.timer(self,1) then +--perpendicular vectors: {-z,x};{z,-x} + local offset=self.collisionbox[1] + local pos1 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=-dir.z*offset,z=dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos1) + if h and h>pos.y then + mobkit.lq_freejump(self) + else + local pos2 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=dir.z*offset,z=-dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos2) + if h and h>pos.y then + mobkit.lq_freejump(self) + 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 + +------------ +-- CALLBACKS +------------ + +function mobkit.default_brain(self) + if mobkit.is_queue_empty_high(self) then mobkit.hq_roam(self,0) 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.lqueue = {} + self.hqueue = {} + self.nearby_objects = {} + self.nearby_players = {} + self.pos_history = {} + self.path_dir = 1 + self.time_total = 0 + + local sdata = minetest.deserialize(staticdata) + if sdata then + for k,v in pairs(sdata) do + self[k] = v + end + end + + if self.timeout and self.timeout>0 and dtime_s > self.timeout and next(self.memory)==nil then + self.object:remove() + 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 + + -- apply texture + if self.texture_no then + local props = {} + props.textures = {self.textures[self.texture_no]} + self.object:set_properties(props) + end + +--hp + self.hp = self.hp or (self.max_hp or 10) +--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.oxygen = self.oxygen or self.lung_capacity + self.lastvelocity = {x=0,y=0,z=0} + self.height = self.collisionbox[5] - self.collisionbox[2] + self.sensefunc=sensors() +end + +function mobkit.stepfunc(self,dtime) -- not intended to be modified + self.dtime = dtime +-- physics comes first +-- self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + local vel = self.object:get_velocity() + +-- if self.lastvelocity.y == vel.y then + if abs(self.lastvelocity.y-vel.y)<0.001 then + self.isonground = true + else + self.isonground = false + end + + -- dumb friction + if self.isonground then + self.object:set_velocity({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 + local vnew = vector.new(vel) + + if not self.collided then -- ugly workaround for inconsistent collisions + 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 + + if not vector.equals(vel,vnew) then + self.collided = true + else + if self.collided then + vnew = vector.new(self.lastvelocity) + end + self.collided = false + end + + self.object:set_velocity(vnew) + end + + -- buoyancy + local spos = mobkit.get_stand_pos(self) + spos.y = spos.y+0.01 + -- get surface height +-- local surface = mobkit.get_node_pos(spos).y+0.5 + local surface = nil + local snodepos = mobkit.get_node_pos(spos) + local surfnode = mobkit.nodeatpos(spos) + while surfnode and surfnode.drawtype == 'liquid' do + surface = snodepos.y+0.5 + if surface > spos.y+self.height then break end + snodepos.y = snodepos.y+1 + surfnode = mobkit.nodeatpos(snodepos) + end + if surface then -- standing in liquid + self.isinliquid = true + local submergence = min(surface-spos.y,self.height) + local balance = self.buoyancy*self.height + local buoyacc = mobkit.gravity*((balance - submergence)^2/balance^2*sign(balance - submergence)) + self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.7,z=-vel.z}) + else + self.isinliquid = false + self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + end + + + + -- local footnode = mobkit.nodeatpos(spos) + -- local headnode + -- if footnode and footnode.drawtype == 'liquid' then + + -- vel = self.object:get_velocity() + -- headnode = mobkit.nodeatpos(mobkit.pos_shift(spos,{y=self.height or 0})) -- TODO: height may be nil + -- local submergence = headnode.drawtype=='liquid' + -- and self.buoyancy-1 + -- or (self.buoyancy*self.height-(1-(spos.y+0.5)%1))^2/(self.buoyancy*self.height)^2*sign(self.buoyancy*self.height-(1-(spos.y+0.5)%1)) + + -- local buoyacc = submergence * mobkit.gravity + -- self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.5,z=-vel.z}) + + -- end + + if self.brainfunc then + -- vitals: fall damage + 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-100) * min(1, velocity_delta/mobkit.terminal_velocity)) + end + + -- vitals: oxygen + local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=self.collisionbox[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 + + + self:sensefunc() + self:brainfunc() + execute_queues(self) + end + + self.lastvelocity = self.object:get_velocity() + self.time_total=self.time_total+self.dtime +end \ No newline at end of file diff --git a/mobkit_api.txt b/mobkit_api.txt new file mode 100644 index 0000000..1a976e6 --- /dev/null +++ b/mobkit_api.txt @@ -0,0 +1,477 @@ +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.2 Brain 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 + +!!! 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.2 Brain 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 | +| | +| ----------------------- | +| | Brain 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 brain 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 + + 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, + brainfunc = [function user defined], + + -- optional mobkit props + -- or used by built in behaviors + + 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], + ... + } + 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 - dtime as passed to on_step + self.hp - hitpoints + self.isonground - true if pos.y remains unchanged for 2 consecutive steps + self.isinliquid - true if the node at foot level is drawtype=='liquid' + +------------ +2. Reference +------------ + +2.1 Utility Functions + +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.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.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.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.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 + + +-- 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 brain functions -- + +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 + + +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 \ No newline at end of file diff --git a/utility.lua b/utility.lua new file mode 100644 index 0000000..331f8e4 --- /dev/null +++ b/utility.lua @@ -0,0 +1,1305 @@ +-- yaw values: +-- x+ = -pi/2 +-- x- = +pi/2 +-- z+ = 0 +-- z- = -pi +--[[ +mobkit={} + +mobkit.gravity = -9.8 +mobkit.friction = 0.4 -- less is more + +local abs = math.abs +local pi = math.pi +local floor = math.floor +local random = math.random +local sqrt = math.sqrt +local max = math.max +local min = math.min +local pow = math.pow + +local sign = function(x) + return (x<0) and -1 or 1 +end + +mobkit.terminal_velocity = sqrt(2*-mobkit.gravity*20) -- 20 meter fall = dead +mobkit.safe_velocity = sqrt(2*-mobkit.gravity*5) -- 5 m safe fall + +local abr = minetest.get_mapgen_setting('active_block_range') + +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} + } + + --]] +-- UTILITY FUNCTIONS + +function mobkit.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 mobkit.neighbor_shift(neighbor,shift) -- int shift: minus is left, plus is right + return (8+neighbor+shift-1)%8+1 +end + +function mobkit.pos_shift(pos,vec) + vec.x=vec.x or 0 + vec.y=vec.y or 0 + vec.z=vec.z or 0 + return {x=pos.x+vec.x, + y=pos.y+vec.y, + z=pos.z+vec.z} +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.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.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.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) + 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() + + local diff = tyaw-yaw + local step = self.dtime*rate + + if abs(diff) pi and -1 or 1 + + local nyaw = diff < 0 and yaw+step*-dirmod or yaw+step*dirmod + + if nyaw > pi then + nyaw=nyaw-pi*2 + elseif nyaw < -pi then + nyaw=nyaw+pi*2 + end + self.object:set_yaw(nyaw) +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.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.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 = (luaent.hp or 0) - dmg + 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 + + self.object:set_animation(aparms.range,aparms.speed,0,aparms.loop) + else + self._anim = nil + end +end + +function mobkit.make_sound(self,sound) + if self.sounds and self.sounds[sound] then + minetest.sound_play(self.sounds[sound], {object=self.object}) + end +end + +function mobkit.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 height, liquidflag = mobkit.get_terrain_height(tpos) + + 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) + 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) + 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.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 + +--[[ + +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 +---------------------------- +-- 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,yaw) + 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 phase=1 + local func=function(self) + if not mobkit.is_alive(target) then return true end + if self.isonground then + if phase==1 then -- collision bug workaround + local vel = self.object:get_velocity() + vel.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) + self.object:set_velocity(vel) + mobkit.make_sound(self,'charge') + phase=2 + else + return true + end + elseif phase==2 then + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + local vy = self.object:get_velocity().y + dir=vector.multiply(dir,6) + dir.y=vy + self.object:set_velocity(dir) + phase=3 + elseif phase==3 then -- in air + local twidth = target:get_properties().collisionbox[1] + local pos = self.object:get_pos() + -- calculate attack spot + local dir = minetest.yaw_to_dir(self.object:get_yaw()) + dir2 = vector.add(dir,self.attack.range+twidth) + local apos = vector.add(pos,dir2) +-- local tpos = mobkit.get_stand_pos(target) --test +-- tpos.y = tpos.y+height + if mobkit.isnear2d(apos,target:get_pos(),0.25) 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') + phase=4 + 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) + 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 + mobkit.lq_idle(self,random(1,6)) +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) + timer=0 + tgttime = 0 + local func = function(self) + if not mobkit.is_alive(tgtobj) then return true 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.brainfunc = 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.8 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 func = function(self) +-- if not self.isinliquid and mobkit.is_queue_empty_low(self) then return true end + if not self.isinliquid and self.isonground then return true end +-- local pos = self.object:get_pos() + local pos = mobkit.get_stand_pos(self) + local y=self.object:get_velocity().y + local pos2d = {x=pos.x,y=0,z=pos.z} + local dir=vector.normalize(vector.direction(pos2d,tpos)) + local yaw = minetest.dir_to_yaw(dir) + + if mobkit.timer(self,1) then +--perpendicular vectors: {-z,x};{z,-x} + local offset=self.collisionbox[1] + local pos1 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=-dir.z*offset,z=dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos1) + if h and h>pos.y then + mobkit.lq_freejump(self) + else + local pos2 = mobkit.pos_shift(mobkit.pos_shift(pos,{x=dir.z*offset,z=-dir.x*offset}),dir) + local h,l = mobkit.get_terrain_height(pos2) + if h and h>pos.y then + mobkit.lq_freejump(self) + 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 + +------------ +-- CALLBACKS +------------ + +function mobkit.default_brain(self) + if mobkit.is_queue_empty_high(self) then mobkit.hq_roam(self,0) 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.lqueue = {} + self.hqueue = {} + self.nearby_objects = {} + self.nearby_players = {} + self.pos_history = {} + self.path_dir = 1 + self.time_total = 0 + + local sdata = minetest.deserialize(staticdata) + if sdata then + for k,v in pairs(sdata) do + self[k] = v + end + end + + if self.timeout and self.timeout>0 and dtime_s > self.timeout and next(self.memory)==nil then + self.object:remove() + 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 + + -- apply texture + if self.texture_no then + local props = {} + props.textures = {self.textures[self.texture_no]} + self.object:set_properties(props) + end + +--hp + self.hp = self.hp or (self.max_hp or 10) +--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.oxygen = self.oxygen or self.lung_capacity + self.lastvelocity = {x=0,y=0,z=0} + self.height = self.collisionbox[5] - self.collisionbox[2] + self.sensefunc=sensors() +end + +function mobkit.stepfunc(self,dtime) -- not intended to be modified + self.dtime = dtime +-- physics comes first +-- self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + local vel = self.object:get_velocity() + +-- if self.lastvelocity.y == vel.y then + if abs(self.lastvelocity.y-vel.y)<0.001 then + self.isonground = true + else + self.isonground = false + end + + -- dumb friction + if self.isonground then + self.object:set_velocity({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 + local vnew = vector.new(vel) + + if not self.collided then -- ugly workaround for inconsistent collisions + 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 + + if not vector.equals(vel,vnew) then + self.collided = true + else + if self.collided then + vnew = vector.new(self.lastvelocity) + end + self.collided = false + end + + self.object:set_velocity(vnew) + end + + -- buoyancy + local spos = mobkit.get_stand_pos(self) + spos.y = spos.y+0.01 + -- get surface height +-- local surface = mobkit.get_node_pos(spos).y+0.5 + local surface = nil + local snodepos = mobkit.get_node_pos(spos) + local surfnode = mobkit.nodeatpos(spos) + while surfnode and surfnode.drawtype == 'liquid' do + surface = snodepos.y+0.5 + if surface > spos.y+self.height then break end + snodepos.y = snodepos.y+1 + surfnode = mobkit.nodeatpos(snodepos) + end + if surface then -- standing in liquid + self.isinliquid = true + local submergence = min(surface-spos.y,self.height) + local balance = self.buoyancy*self.height + local buoyacc = mobkit.gravity*((balance - submergence)^2/balance^2*sign(balance - submergence)) + self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.7,z=-vel.z}) + else + self.isinliquid = false + self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) + end + + + + -- local footnode = mobkit.nodeatpos(spos) + -- local headnode + -- if footnode and footnode.drawtype == 'liquid' then + + -- vel = self.object:get_velocity() + -- headnode = mobkit.nodeatpos(mobkit.pos_shift(spos,{y=self.height or 0})) -- TODO: height may be nil + -- local submergence = headnode.drawtype=='liquid' + -- and self.buoyancy-1 + -- or (self.buoyancy*self.height-(1-(spos.y+0.5)%1))^2/(self.buoyancy*self.height)^2*sign(self.buoyancy*self.height-(1-(spos.y+0.5)%1)) + + -- local buoyacc = submergence * mobkit.gravity + -- self.object:set_acceleration({x=-vel.x,y=buoyacc-vel.y*abs(vel.y)*0.5,z=-vel.z}) + + -- end + + if self.brainfunc then + -- vitals: fall damage + 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-100) * min(1, velocity_delta/mobkit.terminal_velocity)) + end + + -- vitals: oxygen + local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=self.collisionbox[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 + + + self:sensefunc() + self:brainfunc() + execute_queues(self) + end + + self.lastvelocity = self.object:get_velocity() + self.time_total=self.time_total+self.dtime + +end + +--]] \ No newline at end of file