├── LICENSE ├── README.md ├── behaviors2override.lua ├── example_behaviors.lua ├── init.lua ├── mobkit_api.txt ├── mod.conf └── screenshot.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TheTermos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mobkit 2 | Entity API for Minetest 3 | 4 | This library is meant to be shared between mods
5 | Please do not write to the mobkit namespace ('mobkit' global table),
6 | nor include own copies of mobkit in your mods and modpacks. 7 | -------------------------------------------------------------------------------- /behaviors2override.lua: -------------------------------------------------------------------------------- 1 | local abs = math.abs 2 | local pi = math.pi 3 | local floor = math.floor 4 | local ceil = math.ceil 5 | local random = math.random 6 | local sqrt = math.sqrt 7 | local max = math.max 8 | local min = math.min 9 | local tan = math.tan 10 | local pow = math.pow 11 | local dbg = minetest.chat_send_all 12 | 13 | local abr = tonumber(minetest.get_mapgen_setting('active_block_range')) or 3 14 | 15 | local neighbors ={ 16 | {x=1,z=0}, 17 | {x=1,z=1}, 18 | {x=0,z=1}, 19 | {x=-1,z=1}, 20 | {x=-1,z=0}, 21 | {x=-1,z=-1}, 22 | {x=0,z=-1}, 23 | {x=1,z=-1} 24 | } 25 | 26 | function [yournamespace].dir2neighbor(dir) 27 | dir.y=0 28 | dir=vector.round(vector.normalize(dir)) 29 | for k,v in ipairs(neighbors) do 30 | if v.x == dir.x and v.z == dir.z then return k end 31 | end 32 | return 1 33 | end 34 | 35 | function [yournamespace].neighbor_shift(neighbor,shift) -- int shift: minus is left, plus is right 36 | return (8+neighbor+shift-1)%8+1 37 | end 38 | 39 | function [yournamespace].is_neighbor_node_reachable(self,neighbor) -- todo: take either number or pos 40 | local offset = neighbors[neighbor] 41 | local pos=mobkit.get_stand_pos(self) 42 | local tpos = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) 43 | local recursteps = ceil(self.jump_height)+1 44 | local height, liquidflag = mobkit.get_terrain_height(tpos,recursteps) 45 | 46 | if height and abs(height-pos.y) <= self.jump_height then 47 | tpos.y = height 48 | height = height - pos.y 49 | 50 | -- don't cut corners 51 | if neighbor % 2 == 0 then -- diagonal neighbors are even 52 | local n2 = neighbor-1 -- left neighbor never < 0 53 | offset = neighbors[n2] 54 | local t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) 55 | local h2 = mobkit.get_terrain_height(t2,recursteps) 56 | if h2 and h2 - pos.y > 0.02 then return end 57 | n2 = (neighbor+1)%8 -- right neighbor 58 | offset = neighbors[n2] 59 | t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) 60 | h2 = mobkit.get_terrain_height(t2,recursteps) 61 | if h2 and h2 - pos.y > 0.02 then return end 62 | end 63 | 64 | -- check headroom 65 | if tpos.y+self.height-pos.y > 1 then -- if head in next node above, else no point checking headroom 66 | local snpos = mobkit.get_node_pos(pos) 67 | local pos1 = {x=pos.x,y=snpos.y+1,z=pos.z} -- current pos plus node up 68 | local pos2 = {x=tpos.x,y=tpos.y+self.height,z=tpos.z} -- target head pos 69 | 70 | local nodes = mobkit.get_nodes_in_area(pos1,pos2,true) 71 | 72 | for p,node in pairs(nodes) do 73 | if snpos.x==p.x and snpos.z==p.z then 74 | if node.name=='ignore' or node.walkable then return end 75 | else 76 | if node.name=='ignore' or 77 | (node.walkable and mobkit.get_node_height(p)>tpos.y+0.001) then return end 78 | end 79 | end 80 | end 81 | 82 | return height, tpos, liquidflag 83 | else 84 | return 85 | end 86 | end 87 | 88 | function [yournamespace].get_next_waypoint(self,tpos) 89 | local pos = mobkit.get_stand_pos(self) 90 | local dir=vector.direction(pos,tpos) 91 | local neighbor = [yournamespace].dir2neighbor(dir) 92 | local function update_pos_history(self,pos) 93 | table.insert(self.pos_history,1,pos) 94 | if #self.pos_history > 2 then table.remove(self.pos_history,#self.pos_history) end 95 | end 96 | local nogopos = self.pos_history[2] 97 | 98 | local height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) 99 | if height and not liquidflag 100 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 101 | 102 | local heightl = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-1)) 103 | if heightl and abs(heightl-height)<0.001 then 104 | local heightr = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,1)) 105 | if heightr and abs(heightr-height)<0.001 then 106 | dir.y = 0 107 | local dirn = vector.normalize(dir) 108 | local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) 109 | local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) 110 | pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) 111 | end 112 | end 113 | update_pos_history(self,pos2) 114 | return height, pos2 115 | else 116 | 117 | for i=1,3 do 118 | -- scan left 119 | local height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-i*self.path_dir)) 120 | if height and not liq 121 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 122 | update_pos_history(self,pos2) 123 | return height,pos2 124 | end 125 | -- scan right 126 | height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,i*self.path_dir)) 127 | if height and not liq 128 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 129 | update_pos_history(self,pos2) 130 | return height,pos2 131 | end 132 | end 133 | --scan rear 134 | height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,4)) 135 | if height and not liquidflag 136 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 137 | update_pos_history(self,pos2) 138 | return height,pos2 139 | end 140 | end 141 | -- stuck condition here 142 | table.remove(self.pos_history,2) 143 | self.path_dir = self.path_dir*-1 -- subtle change in pathfinding 144 | end 145 | 146 | function [yournamespace].get_next_waypoint_fast(self,tpos,nogopos) 147 | local pos = mobkit.get_stand_pos(self) 148 | local dir=vector.direction(pos,tpos) 149 | local neighbor = [yournamespace].dir2neighbor(dir) 150 | local height, pos2, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) 151 | 152 | if height and not liquidflag then 153 | local fast = false 154 | heightl = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-1)) 155 | if heightl and abs(heightl-height)<0.001 then 156 | heightr = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,1)) 157 | if heightr and abs(heightr-height)<0.001 then 158 | fast = true 159 | dir.y = 0 160 | local dirn = vector.normalize(dir) 161 | local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) 162 | local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) 163 | pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) 164 | end 165 | end 166 | return height, pos2, fast 167 | else 168 | 169 | for i=1,4 do 170 | -- scan left 171 | height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,-i)) 172 | if height and not liq then return height,pos2 end 173 | -- scan right 174 | height, pos2, liq = [yournamespace].is_neighbor_node_reachable(self,[yournamespace].neighbor_shift(neighbor,i)) 175 | if height and not liq then return height,pos2 end 176 | end 177 | end 178 | end 179 | 180 | function [yournamespace].goto_next_waypoint(self,tpos) 181 | local height, pos2 = [yournamespace].get_next_waypoint(self,tpos) 182 | 183 | if not height then return false end 184 | 185 | if height <= 0.01 then 186 | local yaw = self.object:get_yaw() 187 | local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2)) 188 | if abs(tyaw-yaw) > 1 then 189 | [yournamespace].lq_turn2pos(self,pos2) 190 | end 191 | [yournamespace].lq_dumbwalk(self,pos2) 192 | else 193 | [yournamespace].lq_turn2pos(self,pos2) 194 | [yournamespace].lq_dumbjump(self,height) 195 | end 196 | return true 197 | end 198 | 199 | ---------------------------- 200 | -- BEHAVIORS 201 | ---------------------------- 202 | -- LOW LEVEL QUEUE FUNCTIONS 203 | ---------------------------- 204 | 205 | function [yournamespace].lq_turn2pos(self,tpos) 206 | local func=function(self) 207 | local pos = self.object:get_pos() 208 | return mobkit.turn2yaw(self, 209 | minetest.dir_to_yaw(vector.direction(pos,tpos))) 210 | end 211 | mobkit.queue_low(self,func) 212 | end 213 | 214 | function [yournamespace].lq_idle(self,duration,anim) 215 | anim = anim or 'stand' 216 | local init = true 217 | local func=function(self) 218 | if init then 219 | mobkit.animate(self,anim) 220 | init=false 221 | end 222 | duration = duration-self.dtime 223 | if duration <= 0 then return true end 224 | end 225 | mobkit.queue_low(self,func) 226 | end 227 | 228 | function [yournamespace].lq_dumbwalk(self,dest,speed_factor) 229 | local timer = 3 -- failsafe 230 | speed_factor = speed_factor or 1 231 | local func=function(self) 232 | mobkit.animate(self,'walk') 233 | timer = timer - self.dtime 234 | if timer < 0 then return true end 235 | 236 | local pos = mobkit.get_stand_pos(self) 237 | local y = self.object:get_velocity().y 238 | 239 | if mobkit.is_there_yet2d(pos,minetest.yaw_to_dir(self.object:get_yaw()),dest) then 240 | -- if mobkit.isnear2d(pos,dest,0.25) then 241 | if not self.isonground or abs(dest.y-pos.y) > 0.1 then -- prevent uncontrolled fall when velocity too high 242 | -- if abs(dest.y-pos.y) > 0.1 then -- isonground too slow for speeds > 4 243 | self.object:set_velocity({x=0,y=y,z=0}) 244 | end 245 | return true 246 | end 247 | 248 | if self.isonground then 249 | local dir = vector.normalize(vector.direction({x=pos.x,y=0,z=pos.z}, 250 | {x=dest.x,y=0,z=dest.z})) 251 | dir = vector.multiply(dir,self.max_speed*speed_factor) 252 | -- self.object:set_yaw(minetest.dir_to_yaw(dir)) 253 | mobkit.turn2yaw(self,minetest.dir_to_yaw(dir)) 254 | dir.y = y 255 | self.object:set_velocity(dir) 256 | end 257 | end 258 | mobkit.queue_low(self,func) 259 | end 260 | 261 | -- initial velocity for jump height h, v= a*sqrt(h*2/a) ,add 20% 262 | function [yournamespace].lq_dumbjump(self,height,anim) 263 | anim = anim or 'stand' 264 | local jump = true 265 | local func=function(self) 266 | local yaw = self.object:get_yaw() 267 | if self.isonground then 268 | if jump then 269 | mobkit.animate(self,anim) 270 | local dir = minetest.yaw_to_dir(yaw) 271 | dir.y = -mobkit.gravity*sqrt((height+0.35)*2/-mobkit.gravity) 272 | self.object:set_velocity(dir) 273 | jump = false 274 | else -- the eagle has landed 275 | return true 276 | end 277 | else 278 | local dir = minetest.yaw_to_dir(yaw) 279 | local vel = self.object:get_velocity() 280 | if self.lastvelocity.y < 0.9 then 281 | dir = vector.multiply(dir,3) 282 | end 283 | dir.y = vel.y 284 | self.object:set_velocity(dir) 285 | end 286 | end 287 | mobkit.queue_low(self,func) 288 | end 289 | 290 | function [yournamespace].lq_jumpout(self) 291 | local phase = 1 292 | local func=function(self) 293 | local vel=self.object:get_velocity() 294 | if phase == 1 then 295 | vel.y=vel.y+5 296 | self.object:set_velocity(vel) 297 | phase = 2 298 | else 299 | if vel.y < 0 then return true end 300 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 301 | dir.y=vel.y 302 | self.object:set_velocity(dir) 303 | end 304 | end 305 | mobkit.queue_low(self,func) 306 | end 307 | 308 | function [yournamespace].lq_freejump(self) 309 | local phase = 1 310 | local func=function(self) 311 | local vel=self.object:get_velocity() 312 | if phase == 1 then 313 | vel.y=vel.y+6 314 | self.object:set_velocity(vel) 315 | phase = 2 316 | else 317 | if vel.y <= 0.01 then return true end 318 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 319 | dir.y=vel.y 320 | self.object:set_velocity(dir) 321 | end 322 | end 323 | mobkit.queue_low(self,func) 324 | end 325 | 326 | function [yournamespace].lq_jumpattack(self,height,target) 327 | local init=true 328 | local timer=0.5 329 | local tgtbox = target:get_properties().collisionbox 330 | local func=function(self) 331 | if not mobkit.is_alive(target) then return true end 332 | if self.isonground then 333 | if init then -- collision bug workaround 334 | local vel = self.object:get_velocity() 335 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 336 | dir=vector.multiply(dir,6) 337 | dir.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) 338 | self.object:set_velocity(dir) 339 | mobkit.make_sound(self,'charge') 340 | init=false 341 | else 342 | [yournamespace].lq_idle(self,0.3) 343 | return true 344 | end 345 | else 346 | local tgtpos = target:get_pos() 347 | local pos = self.object:get_pos() 348 | -- calculate attack spot 349 | local yaw = self.object:get_yaw() 350 | local dir = minetest.yaw_to_dir(yaw) 351 | local apos = mobkit.pos_translate2d(pos,yaw,self.attack.range) 352 | 353 | if mobkit.is_pos_in_box(apos,tgtpos,tgtbox) then --bite 354 | target:punch(self.object,1,self.attack) 355 | -- bounce off 356 | local vy = self.object:get_velocity().y 357 | self.object:set_velocity({x=dir.x*-3,y=vy,z=dir.z*-3}) 358 | -- play attack sound if defined 359 | mobkit.make_sound(self,'attack') 360 | return true 361 | end 362 | end 363 | end 364 | mobkit.queue_low(self,func) 365 | end 366 | 367 | function [yournamespace].lq_fallover(self) 368 | local zrot = 0 369 | local init = true 370 | local func=function(self) 371 | if init then 372 | local vel = self.object:get_velocity() 373 | self.object:set_velocity(mobkit.pos_shift(vel,{y=1})) 374 | mobkit.animate(self,'stand') 375 | init = false 376 | end 377 | zrot=zrot+pi*0.05 378 | local rot = self.object:get_rotation() 379 | self.object:set_rotation({x=rot.x,y=rot.y,z=zrot}) 380 | if zrot >= pi*0.5 then return true end 381 | end 382 | mobkit.queue_low(self,func) 383 | end 384 | ----------------------------- 385 | -- HIGH LEVEL QUEUE FUNCTIONS 386 | ----------------------------- 387 | 388 | function [yournamespace].dumbstep(self,height,tpos,speed_factor,idle_duration) 389 | if height <= 0.001 then 390 | [yournamespace].lq_turn2pos(self,tpos) 391 | [yournamespace].lq_dumbwalk(self,tpos,speed_factor) 392 | else 393 | [yournamespace].lq_turn2pos(self,tpos) 394 | [yournamespace].lq_dumbjump(self,height) 395 | end 396 | idle_duration = idle_duration or 6 397 | [yournamespace].lq_idle(self,random(ceil(idle_duration*0.5),idle_duration)) 398 | end 399 | 400 | function [yournamespace].hq_roam(self,prty) 401 | local func=function(self) 402 | if mobkit.is_queue_empty_low(self) and self.isonground then 403 | local pos = mobkit.get_stand_pos(self) 404 | local neighbor = random(8) 405 | 406 | local height, tpos, liquidflag = [yournamespace].is_neighbor_node_reachable(self,neighbor) 407 | if height and not liquidflag then [yournamespace].dumbstep(self,height,tpos,0.3) end 408 | end 409 | end 410 | mobkit.queue_high(self,func,prty) 411 | end 412 | 413 | function [yournamespace].hq_follow0(self,tgtobj) -- probably delete this one 414 | local func = function(self) 415 | if not tgtobj then return true end 416 | if mobkit.is_queue_empty_low(self) and self.isonground then 417 | local pos = mobkit.get_stand_pos(self) 418 | local opos = tgtobj:get_pos() 419 | if vector.distance(pos,opos) > 3 then 420 | local neighbor = [yournamespace].dir2neighbor(vector.direction(pos,opos)) 421 | if not neighbor then return true end --temp debug 422 | local height, tpos = [yournamespace].is_neighbor_node_reachable(self,neighbor) 423 | if height then [yournamespace].dumbstep(self,height,tpos) 424 | else 425 | for i=1,4 do --scan left 426 | height, tpos = [yournamespace].is_neighbor_node_reachable(self,(8+neighbor-i-1)%8+1) 427 | if height then [yournamespace].dumbstep(self,height,tpos) 428 | break 429 | end --scan right 430 | height, tpos = [yournamespace].is_neighbor_node_reachable(self,(neighbor+i-1)%8+1) 431 | if height then [yournamespace].dumbstep(self,height,tpos) 432 | break 433 | end 434 | end 435 | end 436 | else 437 | [yournamespace].lq_idle(self,1) 438 | end 439 | end 440 | end 441 | mobkit.queue_high(self,func,0) 442 | end 443 | 444 | function [yournamespace].hq_follow(self,prty,tgtobj) 445 | local func = function(self) 446 | if not mobkit.is_alive(tgtobj) then return true end 447 | if mobkit.is_queue_empty_low(self) and self.isonground then 448 | local pos = mobkit.get_stand_pos(self) 449 | local opos = tgtobj:get_pos() 450 | if vector.distance(pos,opos) > 3 then 451 | [yournamespace].goto_next_waypoint(self,opos) 452 | else 453 | [yournamespace].lq_idle(self,1) 454 | end 455 | end 456 | end 457 | mobkit.queue_high(self,func,prty) 458 | end 459 | 460 | function [yournamespace].hq_goto(self,prty,tpos) 461 | local func = function(self) 462 | if mobkit.is_queue_empty_low(self) and self.isonground then 463 | local pos = mobkit.get_stand_pos(self) 464 | if vector.distance(pos,tpos) > 3 then 465 | [yournamespace].goto_next_waypoint(self,tpos) 466 | else 467 | return true 468 | end 469 | end 470 | end 471 | mobkit.queue_high(self,func,prty) 472 | end 473 | 474 | function [yournamespace].hq_runfrom(self,prty,tgtobj) 475 | local init=true 476 | local timer=6 477 | local func = function(self) 478 | 479 | if not mobkit.is_alive(tgtobj) then return true end 480 | if init then 481 | timer = timer-self.dtime 482 | if timer <=0 or vector.distance(self.object:get_pos(),tgtobj:get_pos()) < 8 then 483 | mobkit.make_sound(self,'scared') 484 | init=false 485 | end 486 | return 487 | end 488 | 489 | if mobkit.is_queue_empty_low(self) and self.isonground then 490 | local pos = mobkit.get_stand_pos(self) 491 | local opos = tgtobj:get_pos() 492 | if vector.distance(pos,opos) < self.view_range*1.1 then 493 | local tpos = {x=2*pos.x - opos.x, 494 | y=opos.y, 495 | z=2*pos.z - opos.z} 496 | [yournamespace].goto_next_waypoint(self,tpos) 497 | else 498 | self.object:set_velocity({x=0,y=0,z=0}) 499 | return true 500 | end 501 | end 502 | end 503 | mobkit.queue_high(self,func,prty) 504 | end 505 | 506 | function [yournamespace].hq_hunt(self,prty,tgtobj) 507 | local func = function(self) 508 | if not mobkit.is_alive(tgtobj) then return true end 509 | if mobkit.is_queue_empty_low(self) and self.isonground then 510 | local pos = mobkit.get_stand_pos(self) 511 | local opos = tgtobj:get_pos() 512 | local dist = vector.distance(pos,opos) 513 | if dist > self.view_range then 514 | return true 515 | elseif dist > 3 then 516 | [yournamespace].goto_next_waypoint(self,opos) 517 | else 518 | [yournamespace].hq_attack(self,prty+1,tgtobj) 519 | end 520 | end 521 | end 522 | mobkit.queue_high(self,func,prty) 523 | end 524 | 525 | function [yournamespace].hq_warn(self,prty,tgtobj) 526 | local timer=0 527 | local tgttime = 0 528 | local init = true 529 | local func = function(self) 530 | if not mobkit.is_alive(tgtobj) then return true end 531 | if init then 532 | mobkit.animate(self,'stand') 533 | init = false 534 | end 535 | local pos = mobkit.get_stand_pos(self) 536 | local opos = tgtobj:get_pos() 537 | local dist = vector.distance(pos,opos) 538 | 539 | if dist > 11 then 540 | return true 541 | elseif dist < 4 or timer > 12 then -- too close man 542 | -- mobkit.clear_queue_high(self) 543 | mobkit.remember(self,'hate',tgtobj:get_player_name()) 544 | [yournamespace].hq_hunt(self,prty+1,tgtobj) -- priority 545 | else 546 | timer = timer+self.dtime 547 | if mobkit.is_queue_empty_low(self) then 548 | [yournamespace].lq_turn2pos(self,opos) 549 | end 550 | -- make noise in random intervals 551 | if timer > tgttime then 552 | mobkit.make_sound(self,'warn') 553 | -- if self.sounds and self.sounds.warn then 554 | -- minetest.sound_play(self.sounds.warn, {object=self.object}) 555 | -- end 556 | tgttime = timer + 1.1 + random()*1.5 557 | end 558 | end 559 | end 560 | mobkit.queue_high(self,func,prty) 561 | end 562 | 563 | function [yournamespace].hq_die(self) 564 | local timer = 5 565 | local start = true 566 | local func = function(self) 567 | if start then 568 | [yournamespace].lq_fallover(self) 569 | self.logic = function(self) end -- brain dead as well 570 | start=false 571 | end 572 | timer = timer-self.dtime 573 | if timer < 0 then self.object:remove() end 574 | end 575 | mobkit.queue_high(self,func,100) 576 | end 577 | 578 | function [yournamespace].hq_attack(self,prty,tgtobj) 579 | local func = function(self) 580 | if not mobkit.is_alive(tgtobj) then return true end 581 | if mobkit.is_queue_empty_low(self) then 582 | local pos = mobkit.get_stand_pos(self) 583 | -- local tpos = tgtobj:get_pos() 584 | local tpos = mobkit.get_stand_pos(tgtobj) 585 | local dist = vector.distance(pos,tpos) 586 | if dist > 3 then 587 | return true 588 | else 589 | [yournamespace].lq_turn2pos(self,tpos) 590 | local height = tgtobj:is_player() and 0.35 or tgtobj:get_luaentity().height*0.6 591 | if tpos.y+height>pos.y then 592 | [yournamespace].lq_jumpattack(self,tpos.y+height-pos.y,tgtobj) 593 | else 594 | [yournamespace].lq_dumbwalk(self,mobkit.pos_shift(tpos,{x=random()-0.5,z=random()-0.5})) 595 | end 596 | end 597 | end 598 | end 599 | mobkit.queue_high(self,func,prty) 600 | end 601 | 602 | function [yournamespace].hq_liquid_recovery(self,prty) -- scan for nearest land 603 | local radius = 1 604 | local yaw = 0 605 | local func = function(self) 606 | if not self.isinliquid then return true end 607 | local pos=self.object:get_pos() 608 | local vec = minetest.yaw_to_dir(yaw) 609 | local pos2 = mobkit.pos_shift(pos,vector.multiply(vec,radius)) 610 | local height, liquidflag = mobkit.get_terrain_height(pos2) 611 | if height and not liquidflag then 612 | [yournamespace].hq_swimto(self,prty,pos2) 613 | return true 614 | end 615 | yaw=yaw+pi*0.25 616 | if yaw>2*pi then 617 | yaw = 0 618 | radius=radius+1 619 | if radius > self.view_range then 620 | self.hp = 0 621 | return true 622 | end 623 | end 624 | end 625 | mobkit.queue_high(self,func,prty) 626 | end 627 | 628 | function [yournamespace].hq_swimto(self,prty,tpos) 629 | local box = self.object:get_properties().collisionbox 630 | local cols = {} 631 | local func = function(self) 632 | if not self.isinliquid then 633 | if self.isonground then return true end 634 | return false 635 | end 636 | 637 | local pos = mobkit.get_stand_pos(self) 638 | local y=self.object:get_velocity().y 639 | local pos2d = {x=pos.x,y=tpos.y,z=pos.z} 640 | local dir=vector.normalize(vector.direction(pos2d,tpos)) 641 | local yaw = minetest.dir_to_yaw(dir) 642 | 643 | if mobkit.timer(self,1) then 644 | cols = mobkit.get_box_displace_cols(pos,box,dir,1) 645 | for _,p in ipairs(cols[1]) do 646 | p.y=pos.y 647 | local h,l = mobkit.get_terrain_height(p) 648 | if h and h>pos.y and self.isinliquid then 649 | [yournamespace].lq_freejump(self) 650 | break 651 | end 652 | end 653 | elseif mobkit.turn2yaw(self,yaw) then 654 | dir.y = y 655 | self.object:set_velocity(dir) 656 | end 657 | end 658 | mobkit.queue_high(self,func,prty) 659 | end 660 | 661 | --------------------- 662 | -- AQUATIC 663 | --------------------- 664 | 665 | -- MACROS 666 | local function aqua_radar_dumb(pos,yaw,range,reverse) 667 | range = range or 4 668 | 669 | local function okpos(p) 670 | local node = mobkit.nodeatpos(p) 671 | if node then 672 | if node.drawtype == 'liquid' then 673 | local nodeu = mobkit.nodeatpos(mobkit.pos_shift(p,{y=1})) 674 | local noded = mobkit.nodeatpos(mobkit.pos_shift(p,{y=-1})) 675 | if (nodeu and nodeu.drawtype == 'liquid') or (noded and noded.drawtype == 'liquid') then 676 | return true 677 | else 678 | return false 679 | end 680 | else 681 | local h,l = mobkit.get_terrain_height(p) 682 | if h then 683 | local node2 = mobkit.nodeatpos({x=p.x,y=h+1.99,z=p.z}) 684 | if node2 and node2.drawtype == 'liquid' then return true, h end 685 | else 686 | return false 687 | end 688 | end 689 | else 690 | return false 691 | end 692 | end 693 | 694 | local fpos = mobkit.pos_translate2d(pos,yaw,range) 695 | local ok,h = okpos(fpos) 696 | if not ok then 697 | local ffrom, fto, fstep 698 | if reverse then 699 | ffrom, fto, fstep = 3,1,-1 700 | else 701 | ffrom, fto, fstep = 1,3,1 702 | end 703 | for i=ffrom, fto, fstep do 704 | local ok,h = okpos(mobkit.pos_translate2d(pos,yaw+i,range)) 705 | if ok then return yaw+i,h end 706 | ok,h = okpos(mobkit.pos_translate2d(pos,yaw-i,range)) 707 | if ok then return yaw-i,h end 708 | end 709 | return yaw+pi,h 710 | else 711 | return yaw, h 712 | end 713 | end 714 | 715 | function [yournamespace].is_in_deep(target) 716 | if not target then return false end 717 | local nodepos = mobkit.get_stand_pos(target) 718 | local node1 = mobkit.nodeatpos(nodepos) 719 | nodepos.y=nodepos.y+1 720 | local node2 = mobkit.nodeatpos(nodepos) 721 | nodepos.y=nodepos.y-2 722 | local node3 = mobkit.nodeatpos(nodepos) 723 | if node1 and node2 and node3 and node1.drawtype=='liquid' and (node2.drawtype=='liquid' or node3.drawtype=='liquid') then 724 | return true 725 | end 726 | end 727 | 728 | -- HQ behaviors 729 | 730 | function [yournamespace].hq_aqua_roam(self,prty,speed) 731 | local tyaw = 0 732 | local init = true 733 | local prvscanpos = {x=0,y=0,z=0} 734 | local center = self.object:get_pos() 735 | local func = function(self) 736 | if init then 737 | mobkit.animate(self,'def') 738 | init = false 739 | end 740 | local pos = mobkit.get_stand_pos(self) 741 | local yaw = self.object:get_yaw() 742 | local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) 743 | if not vector.equals(prvscanpos,scanpos) then 744 | prvscanpos=scanpos 745 | local nyaw,height = aqua_radar_dumb(pos,yaw,speed,true) 746 | if height and height > pos.y then 747 | local vel = self.object:get_velocity() 748 | vel.y = vel.y+1 749 | self.object:set_velocity(vel) 750 | end 751 | if yaw ~= nyaw then 752 | tyaw=nyaw 753 | [yournamespace].hq_aqua_turn(self,prty+1,tyaw,speed) 754 | return 755 | end 756 | end 757 | if mobkit.timer(self,1) then 758 | if vector.distance(pos,center) > abr*16*0.5 then 759 | tyaw = minetest.dir_to_yaw(vector.direction(pos,{x=center.x+random()*10-5,y=center.y,z=center.z+random()*10-5})) 760 | else 761 | if random(10)>=9 then tyaw=tyaw+random()*pi - pi*0.5 end 762 | end 763 | end 764 | 765 | mobkit.turn2yaw(self,tyaw,3) 766 | -- local yaw = self.object:get_yaw() 767 | mobkit.go_forward_horizontal(self,speed) 768 | end 769 | mobkit.queue_high(self,func,prty) 770 | end 771 | 772 | function [yournamespace].hq_aqua_turn(self,prty,tyaw,speed) 773 | local func = function(self) 774 | local finished=mobkit.turn2yaw(self,tyaw) 775 | -- local yaw = self.object:get_yaw() 776 | mobkit.go_forward_horizontal(self,speed) 777 | if finished then return true end 778 | end 779 | mobkit.queue_high(self,func,prty) 780 | end 781 | 782 | function [yournamespace].hq_aqua_attack(self,prty,tgtobj,speed) 783 | local tyaw = 0 784 | local prvscanpos = {x=0,y=0,z=0} 785 | local init = true 786 | local tgtbox = tgtobj:get_properties().collisionbox 787 | local func = function(self) 788 | if not mobkit.is_alive(tgtobj) then return true end 789 | if init then 790 | mobkit.animate(self,'fast') 791 | mobkit.make_sound(self,'attack') 792 | init = false 793 | end 794 | local pos = mobkit.get_stand_pos(self) 795 | local yaw = self.object:get_yaw() 796 | local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) 797 | if not vector.equals(prvscanpos,scanpos) then 798 | prvscanpos=scanpos 799 | local nyaw,height = aqua_radar_dumb(pos,yaw,speed*0.5) 800 | if height and height > pos.y then 801 | local vel = self.object:get_velocity() 802 | vel.y = vel.y+1 803 | self.object:set_velocity(vel) 804 | end 805 | if yaw ~= nyaw then 806 | tyaw=nyaw 807 | [yournamespace].hq_aqua_turn(self,prty+1,tyaw,speed) 808 | return 809 | end 810 | end 811 | 812 | local tpos = tgtobj:get_pos() 813 | local tyaw=minetest.dir_to_yaw(vector.direction(pos,tpos)) 814 | mobkit.turn2yaw(self,tyaw,3) 815 | local yaw = self.object:get_yaw() 816 | if mobkit.timer(self,1) then 817 | if not [yournamespace].is_in_deep(tgtobj) then return true end 818 | local vel = self.object:get_velocity() 819 | if tpos.y>pos.y+0.5 then self.object:set_velocity({x=vel.x,y=vel.y+0.5,z=vel.z}) 820 | elseif tpos.y 0.02 then return end 57 | n2 = (neighbor+1)%8 -- right neighbor 58 | offset = neighbors[n2] 59 | t2 = mobkit.get_node_pos(mobkit.pos_shift(pos,offset)) 60 | h2 = mobkit.get_terrain_height(t2,recursteps) 61 | if h2 and h2 - pos.y > 0.02 then return end 62 | end 63 | 64 | -- check headroom 65 | if tpos.y+self.height-pos.y > 1 then -- if head in next node above, else no point checking headroom 66 | local snpos = mobkit.get_node_pos(pos) 67 | local pos1 = {x=pos.x,y=snpos.y+1,z=pos.z} -- current pos plus node up 68 | local pos2 = {x=tpos.x,y=tpos.y+self.height,z=tpos.z} -- target head pos 69 | 70 | local nodes = mobkit.get_nodes_in_area(pos1,pos2,true) 71 | 72 | for p,node in pairs(nodes) do 73 | if snpos.x==p.x and snpos.z==p.z then 74 | if node.name=='ignore' or node.walkable then return end 75 | else 76 | if node.name=='ignore' or 77 | (node.walkable and mobkit.get_node_height(p)>tpos.y+0.001) then return end 78 | end 79 | end 80 | end 81 | 82 | return height, tpos, liquidflag 83 | else 84 | return 85 | end 86 | end 87 | 88 | function mobkit.get_next_waypoint(self,tpos) 89 | local pos = mobkit.get_stand_pos(self) 90 | local dir=vector.direction(pos,tpos) 91 | local neighbor = mobkit.dir2neighbor(dir) 92 | local function update_pos_history(self,pos) 93 | table.insert(self.pos_history,1,pos) 94 | if #self.pos_history > 2 then table.remove(self.pos_history,#self.pos_history) end 95 | end 96 | local nogopos = self.pos_history[2] 97 | 98 | local height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) 99 | if height and not liquidflag 100 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 101 | 102 | local heightl = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-1)) 103 | if heightl and abs(heightl-height)<0.001 then 104 | local heightr = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,1)) 105 | if heightr and abs(heightr-height)<0.001 then 106 | dir.y = 0 107 | local dirn = vector.normalize(dir) 108 | local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) 109 | local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) 110 | pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) 111 | end 112 | end 113 | update_pos_history(self,pos2) 114 | return height, pos2 115 | else 116 | 117 | for i=1,3 do 118 | -- scan left 119 | local height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-i*self.path_dir)) 120 | if height and not liq 121 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 122 | update_pos_history(self,pos2) 123 | return height,pos2 124 | end 125 | -- scan right 126 | height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,i*self.path_dir)) 127 | if height and not liq 128 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 129 | update_pos_history(self,pos2) 130 | return height,pos2 131 | end 132 | end 133 | --scan rear 134 | height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,4)) 135 | if height and not liquidflag 136 | and not (nogopos and mobkit.isnear2d(pos2,nogopos,0.1)) then 137 | update_pos_history(self,pos2) 138 | return height,pos2 139 | end 140 | end 141 | -- stuck condition here 142 | table.remove(self.pos_history,2) 143 | self.path_dir = self.path_dir*-1 -- subtle change in pathfinding 144 | end 145 | 146 | function mobkit.get_next_waypoint_fast(self,tpos,nogopos) 147 | local pos = mobkit.get_stand_pos(self) 148 | local dir=vector.direction(pos,tpos) 149 | local neighbor = mobkit.dir2neighbor(dir) 150 | local height, pos2, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) 151 | 152 | if height and not liquidflag then 153 | local fast = false 154 | heightl = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-1)) 155 | if heightl and abs(heightl-height)<0.001 then 156 | heightr = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,1)) 157 | if heightr and abs(heightr-height)<0.001 then 158 | fast = true 159 | dir.y = 0 160 | local dirn = vector.normalize(dir) 161 | local npos = mobkit.get_node_pos(mobkit.pos_shift(pos,neighbors[neighbor])) 162 | local factor = abs(dirn.x) > abs(dirn.z) and abs(npos.x-pos.x) or abs(npos.z-pos.z) 163 | pos2=mobkit.pos_shift(pos,{x=dirn.x*factor,z=dirn.z*factor}) 164 | end 165 | end 166 | return height, pos2, fast 167 | else 168 | 169 | for i=1,4 do 170 | -- scan left 171 | height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,-i)) 172 | if height and not liq then return height,pos2 end 173 | -- scan right 174 | height, pos2, liq = mobkit.is_neighbor_node_reachable(self,mobkit.neighbor_shift(neighbor,i)) 175 | if height and not liq then return height,pos2 end 176 | end 177 | end 178 | end 179 | 180 | function mobkit.goto_next_waypoint(self,tpos) 181 | local height, pos2 = mobkit.get_next_waypoint(self,tpos) 182 | 183 | if not height then return false end 184 | 185 | if height <= 0.01 then 186 | local yaw = self.object:get_yaw() 187 | local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2)) 188 | if abs(tyaw-yaw) > 1 then 189 | mobkit.lq_turn2pos(self,pos2) 190 | end 191 | mobkit.lq_dumbwalk(self,pos2) 192 | else 193 | mobkit.lq_turn2pos(self,pos2) 194 | mobkit.lq_dumbjump(self,height) 195 | end 196 | return true 197 | end 198 | 199 | ---------------------------- 200 | -- BEHAVIORS 201 | ---------------------------- 202 | -- LOW LEVEL QUEUE FUNCTIONS 203 | ---------------------------- 204 | 205 | function mobkit.lq_turn2pos(self,tpos) 206 | local func=function(self) 207 | local pos = self.object:get_pos() 208 | return mobkit.turn2yaw(self, 209 | minetest.dir_to_yaw(vector.direction(pos,tpos))) 210 | end 211 | mobkit.queue_low(self,func) 212 | end 213 | 214 | function mobkit.lq_idle(self,duration,anim) 215 | anim = anim or 'stand' 216 | local init = true 217 | local func=function(self) 218 | if init then 219 | mobkit.animate(self,anim) 220 | init=false 221 | end 222 | duration = duration-self.dtime 223 | if duration <= 0 then return true end 224 | end 225 | mobkit.queue_low(self,func) 226 | end 227 | 228 | function mobkit.lq_dumbwalk(self,dest,speed_factor) 229 | local timer = 3 -- failsafe 230 | speed_factor = speed_factor or 1 231 | local func=function(self) 232 | mobkit.animate(self,'walk') 233 | timer = timer - self.dtime 234 | if timer < 0 then return true end 235 | 236 | local pos = mobkit.get_stand_pos(self) 237 | local y = self.object:get_velocity().y 238 | 239 | if mobkit.is_there_yet2d(pos,minetest.yaw_to_dir(self.object:get_yaw()),dest) then 240 | -- if mobkit.isnear2d(pos,dest,0.25) then 241 | if not self.isonground or abs(dest.y-pos.y) > 0.1 then -- prevent uncontrolled fall when velocity too high 242 | -- if abs(dest.y-pos.y) > 0.1 then -- isonground too slow for speeds > 4 243 | self.object:set_velocity({x=0,y=y,z=0}) 244 | end 245 | return true 246 | end 247 | 248 | if self.isonground then 249 | local dir = vector.normalize(vector.direction({x=pos.x,y=0,z=pos.z}, 250 | {x=dest.x,y=0,z=dest.z})) 251 | dir = vector.multiply(dir,self.max_speed*speed_factor) 252 | -- self.object:set_yaw(minetest.dir_to_yaw(dir)) 253 | mobkit.turn2yaw(self,minetest.dir_to_yaw(dir)) 254 | dir.y = y 255 | self.object:set_velocity(dir) 256 | end 257 | end 258 | mobkit.queue_low(self,func) 259 | end 260 | 261 | -- initial velocity for jump height h, v= a*sqrt(h*2/a) ,add 20% 262 | function mobkit.lq_dumbjump(self,height,anim) 263 | anim = anim or 'stand' 264 | local jump = true 265 | local func=function(self) 266 | local yaw = self.object:get_yaw() 267 | if self.isonground then 268 | if jump then 269 | mobkit.animate(self,anim) 270 | local dir = minetest.yaw_to_dir(yaw) 271 | dir.y = -mobkit.gravity*sqrt((height+0.35)*2/-mobkit.gravity) 272 | self.object:set_velocity(dir) 273 | jump = false 274 | else -- the eagle has landed 275 | return true 276 | end 277 | else 278 | local dir = minetest.yaw_to_dir(yaw) 279 | local vel = self.object:get_velocity() 280 | if self.lastvelocity.y < 0.9 then 281 | dir = vector.multiply(dir,3) 282 | end 283 | dir.y = vel.y 284 | self.object:set_velocity(dir) 285 | end 286 | end 287 | mobkit.queue_low(self,func) 288 | end 289 | 290 | function mobkit.lq_jumpout(self) 291 | local phase = 1 292 | local func=function(self) 293 | local vel=self.object:get_velocity() 294 | if phase == 1 then 295 | vel.y=vel.y+5 296 | self.object:set_velocity(vel) 297 | phase = 2 298 | else 299 | if vel.y < 0 then return true end 300 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 301 | dir.y=vel.y 302 | self.object:set_velocity(dir) 303 | end 304 | end 305 | mobkit.queue_low(self,func) 306 | end 307 | 308 | function mobkit.lq_freejump(self) 309 | local phase = 1 310 | local func=function(self) 311 | local vel=self.object:get_velocity() 312 | if phase == 1 then 313 | vel.y=vel.y+6 314 | self.object:set_velocity(vel) 315 | phase = 2 316 | else 317 | if vel.y <= 0.01 then return true end 318 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 319 | dir.y=vel.y 320 | self.object:set_velocity(dir) 321 | end 322 | end 323 | mobkit.queue_low(self,func) 324 | end 325 | 326 | function mobkit.lq_jumpattack(self,height,target) 327 | local init=true 328 | local timer=0.5 329 | local tgtbox = target:get_properties().collisionbox 330 | local func=function(self) 331 | if not mobkit.is_alive(target) then return true end 332 | if self.isonground then 333 | if init then -- collision bug workaround 334 | local vel = self.object:get_velocity() 335 | local dir = minetest.yaw_to_dir(self.object:get_yaw()) 336 | dir=vector.multiply(dir,6) 337 | dir.y = -mobkit.gravity*sqrt(height*2/-mobkit.gravity) 338 | self.object:set_velocity(dir) 339 | mobkit.make_sound(self,'charge') 340 | init=false 341 | else 342 | mobkit.lq_idle(self,0.3) 343 | return true 344 | end 345 | else 346 | local tgtpos = target:get_pos() 347 | local pos = self.object:get_pos() 348 | -- calculate attack spot 349 | local yaw = self.object:get_yaw() 350 | local dir = minetest.yaw_to_dir(yaw) 351 | local apos = mobkit.pos_translate2d(pos,yaw,self.attack.range) 352 | 353 | if mobkit.is_pos_in_box(apos,tgtpos,tgtbox) then --bite 354 | target:punch(self.object,1,self.attack) 355 | -- bounce off 356 | local vy = self.object:get_velocity().y 357 | self.object:set_velocity({x=dir.x*-3,y=vy,z=dir.z*-3}) 358 | -- play attack sound if defined 359 | mobkit.make_sound(self,'attack') 360 | return true 361 | end 362 | end 363 | end 364 | mobkit.queue_low(self,func) 365 | end 366 | 367 | function mobkit.lq_fallover(self) 368 | local zrot = 0 369 | local init = true 370 | local func=function(self) 371 | if init then 372 | local vel = self.object:get_velocity() 373 | self.object:set_velocity(mobkit.pos_shift(vel,{y=1})) 374 | mobkit.animate(self,'stand') 375 | init = false 376 | end 377 | zrot=zrot+pi*0.05 378 | local rot = self.object:get_rotation() 379 | self.object:set_rotation({x=rot.x,y=rot.y,z=zrot}) 380 | if zrot >= pi*0.5 then return true end 381 | end 382 | mobkit.queue_low(self,func) 383 | end 384 | ----------------------------- 385 | -- HIGH LEVEL QUEUE FUNCTIONS 386 | ----------------------------- 387 | 388 | function mobkit.dumbstep(self,height,tpos,speed_factor,idle_duration) 389 | if height <= 0.001 then 390 | mobkit.lq_turn2pos(self,tpos) 391 | mobkit.lq_dumbwalk(self,tpos,speed_factor) 392 | else 393 | mobkit.lq_turn2pos(self,tpos) 394 | mobkit.lq_dumbjump(self,height) 395 | end 396 | idle_duration = idle_duration or 6 397 | mobkit.lq_idle(self,random(ceil(idle_duration*0.5),idle_duration)) 398 | end 399 | 400 | function mobkit.hq_roam(self,prty) 401 | local func=function(self) 402 | if mobkit.is_queue_empty_low(self) and self.isonground then 403 | local pos = mobkit.get_stand_pos(self) 404 | local neighbor = random(8) 405 | 406 | local height, tpos, liquidflag = mobkit.is_neighbor_node_reachable(self,neighbor) 407 | if height and not liquidflag then mobkit.dumbstep(self,height,tpos,0.3) end 408 | end 409 | end 410 | mobkit.queue_high(self,func,prty) 411 | end 412 | 413 | function mobkit.hq_follow0(self,tgtobj) -- probably delete this one 414 | local func = function(self) 415 | if not tgtobj then return true end 416 | if mobkit.is_queue_empty_low(self) and self.isonground then 417 | local pos = mobkit.get_stand_pos(self) 418 | local opos = tgtobj:get_pos() 419 | if vector.distance(pos,opos) > 3 then 420 | local neighbor = mobkit.dir2neighbor(vector.direction(pos,opos)) 421 | if not neighbor then return true end --temp debug 422 | local height, tpos = mobkit.is_neighbor_node_reachable(self,neighbor) 423 | if height then mobkit.dumbstep(self,height,tpos) 424 | else 425 | for i=1,4 do --scan left 426 | height, tpos = mobkit.is_neighbor_node_reachable(self,(8+neighbor-i-1)%8+1) 427 | if height then mobkit.dumbstep(self,height,tpos) 428 | break 429 | end --scan right 430 | height, tpos = mobkit.is_neighbor_node_reachable(self,(neighbor+i-1)%8+1) 431 | if height then mobkit.dumbstep(self,height,tpos) 432 | break 433 | end 434 | end 435 | end 436 | else 437 | mobkit.lq_idle(self,1) 438 | end 439 | end 440 | end 441 | mobkit.queue_high(self,func,0) 442 | end 443 | 444 | function mobkit.hq_follow(self,prty,tgtobj) 445 | local func = function(self) 446 | if not mobkit.is_alive(tgtobj) then return true end 447 | if mobkit.is_queue_empty_low(self) and self.isonground then 448 | local pos = mobkit.get_stand_pos(self) 449 | local opos = tgtobj:get_pos() 450 | if vector.distance(pos,opos) > 3 then 451 | mobkit.goto_next_waypoint(self,opos) 452 | else 453 | mobkit.lq_idle(self,1) 454 | end 455 | end 456 | end 457 | mobkit.queue_high(self,func,prty) 458 | end 459 | 460 | function mobkit.hq_goto(self,prty,tpos) 461 | local func = function(self) 462 | if mobkit.is_queue_empty_low(self) and self.isonground then 463 | local pos = mobkit.get_stand_pos(self) 464 | if vector.distance(pos,tpos) > 3 then 465 | mobkit.goto_next_waypoint(self,tpos) 466 | else 467 | return true 468 | end 469 | end 470 | end 471 | mobkit.queue_high(self,func,prty) 472 | end 473 | 474 | function mobkit.hq_runfrom(self,prty,tgtobj) 475 | local init=true 476 | local timer=6 477 | local func = function(self) 478 | 479 | if not mobkit.is_alive(tgtobj) then return true end 480 | if init then 481 | timer = timer-self.dtime 482 | if timer <=0 or vector.distance(self.object:get_pos(),tgtobj:get_pos()) < 8 then 483 | mobkit.make_sound(self,'scared') 484 | init=false 485 | end 486 | return 487 | end 488 | 489 | if mobkit.is_queue_empty_low(self) and self.isonground then 490 | local pos = mobkit.get_stand_pos(self) 491 | local opos = tgtobj:get_pos() 492 | if vector.distance(pos,opos) < self.view_range*1.1 then 493 | local tpos = {x=2*pos.x - opos.x, 494 | y=opos.y, 495 | z=2*pos.z - opos.z} 496 | mobkit.goto_next_waypoint(self,tpos) 497 | else 498 | self.object:set_velocity({x=0,y=0,z=0}) 499 | return true 500 | end 501 | end 502 | end 503 | mobkit.queue_high(self,func,prty) 504 | end 505 | 506 | function mobkit.hq_hunt(self,prty,tgtobj) 507 | local func = function(self) 508 | if not mobkit.is_alive(tgtobj) then return true end 509 | if mobkit.is_queue_empty_low(self) and self.isonground then 510 | local pos = mobkit.get_stand_pos(self) 511 | local opos = tgtobj:get_pos() 512 | local dist = vector.distance(pos,opos) 513 | if dist > self.view_range then 514 | return true 515 | elseif dist > 3 then 516 | mobkit.goto_next_waypoint(self,opos) 517 | else 518 | mobkit.hq_attack(self,prty+1,tgtobj) 519 | end 520 | end 521 | end 522 | mobkit.queue_high(self,func,prty) 523 | end 524 | 525 | function mobkit.hq_warn(self,prty,tgtobj) 526 | local timer=0 527 | local tgttime = 0 528 | local init = true 529 | local func = function(self) 530 | if not mobkit.is_alive(tgtobj) then return true end 531 | if init then 532 | mobkit.animate(self,'stand') 533 | init = false 534 | end 535 | local pos = mobkit.get_stand_pos(self) 536 | local opos = tgtobj:get_pos() 537 | local dist = vector.distance(pos,opos) 538 | 539 | if dist > 11 then 540 | return true 541 | elseif dist < 4 or timer > 12 then -- too close man 542 | -- mobkit.clear_queue_high(self) 543 | mobkit.remember(self,'hate',tgtobj:get_player_name()) 544 | mobkit.hq_hunt(self,prty+1,tgtobj) -- priority 545 | else 546 | timer = timer+self.dtime 547 | if mobkit.is_queue_empty_low(self) then 548 | mobkit.lq_turn2pos(self,opos) 549 | end 550 | -- make noise in random intervals 551 | if timer > tgttime then 552 | mobkit.make_sound(self,'warn') 553 | -- if self.sounds and self.sounds.warn then 554 | -- minetest.sound_play(self.sounds.warn, {object=self.object}) 555 | -- end 556 | tgttime = timer + 1.1 + random()*1.5 557 | end 558 | end 559 | end 560 | mobkit.queue_high(self,func,prty) 561 | end 562 | 563 | function mobkit.hq_die(self) 564 | local timer = 5 565 | local start = true 566 | local func = function(self) 567 | if start then 568 | mobkit.lq_fallover(self) 569 | self.logic = function(self) end -- brain dead as well 570 | start=false 571 | end 572 | timer = timer-self.dtime 573 | if timer < 0 then self.object:remove() end 574 | end 575 | mobkit.queue_high(self,func,100) 576 | end 577 | 578 | function mobkit.hq_attack(self,prty,tgtobj) 579 | local func = function(self) 580 | if not mobkit.is_alive(tgtobj) then return true end 581 | if mobkit.is_queue_empty_low(self) then 582 | local pos = mobkit.get_stand_pos(self) 583 | -- local tpos = tgtobj:get_pos() 584 | local tpos = mobkit.get_stand_pos(tgtobj) 585 | local dist = vector.distance(pos,tpos) 586 | if dist > 3 then 587 | return true 588 | else 589 | mobkit.lq_turn2pos(self,tpos) 590 | local height = tgtobj:is_player() and 0.35 or tgtobj:get_luaentity().height*0.6 591 | if tpos.y+height>pos.y then 592 | mobkit.lq_jumpattack(self,tpos.y+height-pos.y,tgtobj) 593 | else 594 | mobkit.lq_dumbwalk(self,mobkit.pos_shift(tpos,{x=random()-0.5,z=random()-0.5})) 595 | end 596 | end 597 | end 598 | end 599 | mobkit.queue_high(self,func,prty) 600 | end 601 | 602 | function mobkit.hq_liquid_recovery(self,prty) -- scan for nearest land 603 | local radius = 1 604 | local yaw = 0 605 | local func = function(self) 606 | if not self.isinliquid then return true end 607 | local pos=self.object:get_pos() 608 | local vec = minetest.yaw_to_dir(yaw) 609 | local pos2 = mobkit.pos_shift(pos,vector.multiply(vec,radius)) 610 | local height, liquidflag = mobkit.get_terrain_height(pos2) 611 | if height and not liquidflag then 612 | mobkit.hq_swimto(self,prty,pos2) 613 | return true 614 | end 615 | yaw=yaw+pi*0.25 616 | if yaw>2*pi then 617 | yaw = 0 618 | radius=radius+1 619 | if radius > self.view_range then 620 | self.hp = 0 621 | return true 622 | end 623 | end 624 | end 625 | mobkit.queue_high(self,func,prty) 626 | end 627 | 628 | function mobkit.hq_swimto(self,prty,tpos) 629 | local box = self.object:get_properties().collisionbox 630 | local cols = {} 631 | local func = function(self) 632 | if not self.isinliquid then 633 | if self.isonground then return true end 634 | return false 635 | end 636 | 637 | local pos = mobkit.get_stand_pos(self) 638 | local y=self.object:get_velocity().y 639 | local pos2d = {x=pos.x,y=tpos.y,z=pos.z} 640 | local dir=vector.normalize(vector.direction(pos2d,tpos)) 641 | local yaw = minetest.dir_to_yaw(dir) 642 | 643 | if mobkit.timer(self,1) then 644 | cols = mobkit.get_box_displace_cols(pos,box,dir,1) 645 | for _,p in ipairs(cols[1]) do 646 | p.y=pos.y 647 | local h,l = mobkit.get_terrain_height(p) 648 | if h and h>pos.y and self.isinliquid then 649 | mobkit.lq_freejump(self) 650 | break 651 | end 652 | end 653 | elseif mobkit.turn2yaw(self,yaw) then 654 | dir.y = y 655 | self.object:set_velocity(dir) 656 | end 657 | end 658 | mobkit.queue_high(self,func,prty) 659 | end 660 | 661 | --------------------- 662 | -- AQUATIC 663 | --------------------- 664 | 665 | -- MACROS 666 | local function aqua_radar_dumb(pos,yaw,range,reverse) 667 | range = range or 4 668 | 669 | local function okpos(p) 670 | local node = mobkit.nodeatpos(p) 671 | if node then 672 | if node.drawtype == 'liquid' then 673 | local nodeu = mobkit.nodeatpos(mobkit.pos_shift(p,{y=1})) 674 | local noded = mobkit.nodeatpos(mobkit.pos_shift(p,{y=-1})) 675 | if (nodeu and nodeu.drawtype == 'liquid') or (noded and noded.drawtype == 'liquid') then 676 | return true 677 | else 678 | return false 679 | end 680 | else 681 | local h,l = mobkit.get_terrain_height(p) 682 | if h then 683 | local node2 = mobkit.nodeatpos({x=p.x,y=h+1.99,z=p.z}) 684 | if node2 and node2.drawtype == 'liquid' then return true, h end 685 | else 686 | return false 687 | end 688 | end 689 | else 690 | return false 691 | end 692 | end 693 | 694 | local fpos = mobkit.pos_translate2d(pos,yaw,range) 695 | local ok,h = okpos(fpos) 696 | if not ok then 697 | local ffrom, fto, fstep 698 | if reverse then 699 | ffrom, fto, fstep = 3,1,-1 700 | else 701 | ffrom, fto, fstep = 1,3,1 702 | end 703 | for i=ffrom, fto, fstep do 704 | local ok,h = okpos(mobkit.pos_translate2d(pos,yaw+i,range)) 705 | if ok then return yaw+i,h end 706 | ok,h = okpos(mobkit.pos_translate2d(pos,yaw-i,range)) 707 | if ok then return yaw-i,h end 708 | end 709 | return yaw+pi,h 710 | else 711 | return yaw, h 712 | end 713 | end 714 | 715 | function mobkit.is_in_deep(target) 716 | if not target then return false end 717 | local nodepos = mobkit.get_stand_pos(target) 718 | local node1 = mobkit.nodeatpos(nodepos) 719 | nodepos.y=nodepos.y+1 720 | local node2 = mobkit.nodeatpos(nodepos) 721 | nodepos.y=nodepos.y-2 722 | local node3 = mobkit.nodeatpos(nodepos) 723 | if node1 and node2 and node3 and node1.drawtype=='liquid' and (node2.drawtype=='liquid' or node3.drawtype=='liquid') then 724 | return true 725 | end 726 | end 727 | 728 | -- HQ behaviors 729 | 730 | function mobkit.hq_aqua_roam(self,prty,speed) 731 | local tyaw = 0 732 | local init = true 733 | local prvscanpos = {x=0,y=0,z=0} 734 | local center = self.object:get_pos() 735 | local func = function(self) 736 | if init then 737 | mobkit.animate(self,'def') 738 | init = false 739 | end 740 | local pos = mobkit.get_stand_pos(self) 741 | local yaw = self.object:get_yaw() 742 | local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) 743 | if not vector.equals(prvscanpos,scanpos) then 744 | prvscanpos=scanpos 745 | local nyaw,height = aqua_radar_dumb(pos,yaw,speed,true) 746 | if height and height > pos.y then 747 | local vel = self.object:get_velocity() 748 | vel.y = vel.y+1 749 | self.object:set_velocity(vel) 750 | end 751 | if yaw ~= nyaw then 752 | tyaw=nyaw 753 | mobkit.hq_aqua_turn(self,prty+1,tyaw,speed) 754 | return 755 | end 756 | end 757 | if mobkit.timer(self,1) then 758 | if vector.distance(pos,center) > abr*16*0.5 then 759 | tyaw = minetest.dir_to_yaw(vector.direction(pos,{x=center.x+random()*10-5,y=center.y,z=center.z+random()*10-5})) 760 | else 761 | if random(10)>=9 then tyaw=tyaw+random()*pi - pi*0.5 end 762 | end 763 | end 764 | 765 | mobkit.turn2yaw(self,tyaw,3) 766 | -- local yaw = self.object:get_yaw() 767 | mobkit.go_forward_horizontal(self,speed) 768 | end 769 | mobkit.queue_high(self,func,prty) 770 | end 771 | 772 | function mobkit.hq_aqua_turn(self,prty,tyaw,speed) 773 | local func = function(self) 774 | local finished=mobkit.turn2yaw(self,tyaw) 775 | -- local yaw = self.object:get_yaw() 776 | mobkit.go_forward_horizontal(self,speed) 777 | if finished then return true end 778 | end 779 | mobkit.queue_high(self,func,prty) 780 | end 781 | 782 | function mobkit.hq_aqua_attack(self,prty,tgtobj,speed) 783 | local tyaw = 0 784 | local prvscanpos = {x=0,y=0,z=0} 785 | local init = true 786 | local tgtbox = tgtobj:get_properties().collisionbox 787 | local func = function(self) 788 | if not mobkit.is_alive(tgtobj) then return true end 789 | if init then 790 | mobkit.animate(self,'fast') 791 | mobkit.make_sound(self,'attack') 792 | init = false 793 | end 794 | local pos = mobkit.get_stand_pos(self) 795 | local yaw = self.object:get_yaw() 796 | local scanpos = mobkit.get_node_pos(mobkit.pos_translate2d(pos,yaw,speed)) 797 | if not vector.equals(prvscanpos,scanpos) then 798 | prvscanpos=scanpos 799 | local nyaw,height = aqua_radar_dumb(pos,yaw,speed*0.5) 800 | if height and height > pos.y then 801 | local vel = self.object:get_velocity() 802 | vel.y = vel.y+1 803 | self.object:set_velocity(vel) 804 | end 805 | if yaw ~= nyaw then 806 | tyaw=nyaw 807 | mobkit.hq_aqua_turn(self,prty+1,tyaw,speed) 808 | return 809 | end 810 | end 811 | 812 | local tpos = tgtobj:get_pos() 813 | local tyaw=minetest.dir_to_yaw(vector.direction(pos,tpos)) 814 | mobkit.turn2yaw(self,tyaw,3) 815 | local yaw = self.object:get_yaw() 816 | if mobkit.timer(self,1) then 817 | if not mobkit.is_in_deep(tgtobj) then return true end 818 | local vel = self.object:get_velocity() 819 | if tpos.y>pos.y+0.5 then self.object:set_velocity({x=vel.x,y=vel.y+0.5,z=vel.z}) 820 | elseif tpos.y bpos.x+box[1] and pos.x < bpos.x+box[4] and 57 | pos.y > bpos.y+box[2] and pos.y < bpos.y+box[5] and 58 | pos.z > bpos.z+box[3] and pos.z < bpos.z+box[6] 59 | end 60 | 61 | -- call this instead if you want feet position. 62 | --[[ 63 | function mobkit.get_stand_pos(thing) -- thing can be luaentity or objectref. 64 | if type(thing) == 'table' then 65 | return mobkit.pos_shift(thing.object:get_pos(),{y=thing.collisionbox[2]+0.01}) 66 | elseif type(thing) == 'userdata' then 67 | local colbox = thing:get_properties().collisionbox 68 | return mobkit.pos_shift(thing:get_pos(),{y=colbox[2]+0.01}) 69 | end 70 | end --]] 71 | 72 | function mobkit.get_stand_pos(thing) -- thing can be luaentity or objectref. 73 | local pos = {} 74 | local colbox = {} 75 | if type(thing) == 'table' then 76 | pos = thing.object:get_pos() 77 | colbox = thing.object:get_properties().collisionbox 78 | elseif type(thing) == 'userdata' then 79 | pos = thing:get_pos() 80 | colbox = thing:get_properties().collisionbox 81 | else 82 | return false 83 | end 84 | return mobkit.pos_shift(pos,{y=colbox[2]+0.01}), pos 85 | end 86 | 87 | function mobkit.set_acceleration(thing,vec,limit) 88 | limit = limit or 100 89 | if type(thing) == 'table' then thing=thing.object end 90 | vec.x=mobkit.minmax(vec.x,limit) 91 | vec.y=mobkit.minmax(vec.y,limit) 92 | vec.z=mobkit.minmax(vec.z,limit) 93 | 94 | thing:set_acceleration(vec) 95 | end 96 | 97 | function mobkit.nodeatpos(pos) 98 | local node = minetest.get_node_or_nil(pos) 99 | if node then return minetest.registered_nodes[node.name] end 100 | end 101 | 102 | function mobkit.get_nodename_off(pos,vec) 103 | return minetest.get_node(mobkit.pos_shift(pos,vec)).name 104 | end 105 | 106 | function mobkit.get_node_pos(pos) 107 | return { 108 | x=floor(pos.x+0.5), 109 | y=floor(pos.y+0.5), 110 | z=floor(pos.z+0.5), 111 | } 112 | end 113 | 114 | function mobkit.get_nodes_in_area(pos1,pos2,full) 115 | local npos1=mobkit.get_node_pos(pos1) 116 | local npos2=mobkit.get_node_pos(pos2) 117 | local result = {} 118 | local cnt = 0 -- safety 119 | 120 | local sx = (pos2.x 125 then 149 | minetest.chat_send_all('get_nodes_in_area: area too big ') 150 | return result 151 | end 152 | 153 | until y==npos2.y 154 | until z==npos2.z 155 | until x==npos2.x 156 | 157 | return result 158 | end 159 | 160 | function mobkit.get_hitbox_bottom(self) 161 | local y = self.collisionbox[2] 162 | local pos = self.object:get_pos() 163 | return { 164 | {x=pos.x+self.collisionbox[1],y=pos.y+y,z=pos.z+self.collisionbox[3]}, 165 | {x=pos.x+self.collisionbox[1],y=pos.y+y,z=pos.z+self.collisionbox[6]}, 166 | {x=pos.x+self.collisionbox[4],y=pos.y+y,z=pos.z+self.collisionbox[3]}, 167 | {x=pos.x+self.collisionbox[4],y=pos.y+y,z=pos.z+self.collisionbox[6]}, 168 | } 169 | end 170 | 171 | function mobkit.get_node_height(pos) 172 | local npos = mobkit.get_node_pos(pos) 173 | local node = mobkit.nodeatpos(npos) 174 | if node == nil then return nil end 175 | 176 | if node.walkable then 177 | if node.drawtype == 'nodebox' then 178 | if node.node_box and node.node_box.type == 'fixed' then 179 | if type(node.node_box.fixed[1]) == 'number' then 180 | return npos.y + node.node_box.fixed[5] ,0, false 181 | elseif type(node.node_box.fixed[1]) == 'table' then 182 | return npos.y + node.node_box.fixed[1][5] ,0, false 183 | else 184 | return npos.y + 0.5,1, false -- todo handle table of boxes 185 | end 186 | elseif node.node_box and node.node_box.type == 'leveled' then 187 | return minetest.get_node_level(pos)/64-0.5+mobkit.get_node_pos(pos).y, 0, false 188 | else 189 | return npos.y + 0.5,1, false -- the unforeseen 190 | end 191 | else 192 | return npos.y+0.5,1, false -- full node 193 | end 194 | else 195 | local liquidflag = false 196 | if node.drawtype == 'liquid' then liquidflag = true end 197 | return npos.y-0.5,-1,liquidflag 198 | end 199 | end 200 | 201 | -- get_terrain_height 202 | -- steps(optional) number of recursion steps; default=3 203 | -- dir(optional) is 1=up, -1=down, 0=both; default=0 204 | -- liquidflag(forbidden) never provide this parameter. 205 | function mobkit.get_terrain_height(pos,steps,dir,liquidflag) --dir is 1=up, -1=down, 0=both 206 | steps = steps or 3 207 | dir = dir or 0 208 | 209 | local h,f,l = mobkit.get_node_height(pos) 210 | if h == nil then return nil end 211 | if l then liquidflag = true end 212 | 213 | if f==0 then 214 | return h, liquidflag 215 | end 216 | 217 | if dir==0 or dir==f then 218 | steps = steps - 1 219 | if steps <=0 then return nil end 220 | return mobkit.get_terrain_height(mobkit.pos_shift(pos,{y=f}),steps,f,liquidflag) 221 | else 222 | return h, liquidflag 223 | end 224 | end 225 | 226 | function mobkit.get_spawn_pos_abr(dtime,intrvl,radius,chance,reduction) 227 | dtime = min(dtime,0.1) 228 | local plyrs = minetest.get_connected_players() 229 | intrvl=1/intrvl 230 | 231 | if random() 1 then 239 | -- spawn in the front arc 240 | yaw = minetest.dir_to_yaw(vel) + random()*0.35 - 0.75 241 | else 242 | -- random yaw 243 | yaw = random()*pi*2 - pi 244 | end 245 | local pos = plyr:get_pos() 246 | local dir = vector.multiply(minetest.yaw_to_dir(yaw),radius) 247 | local pos2 = vector.add(pos,dir) 248 | pos2.y=pos2.y-5 249 | local height, liquidflag = mobkit.get_terrain_height(pos2,32) 250 | if height then 251 | local objs = minetest.get_objects_inside_radius(pos,radius*1.1) 252 | for _,obj in ipairs(objs) do -- count mobs in abrange 253 | if not obj:is_player() then 254 | local lua = obj:get_luaentity() 255 | if lua and lua.name ~= '__builtin:item' then 256 | chance=chance + (1-chance)*reduction -- chance reduced for every mob in range 257 | end 258 | end 259 | end 260 | if chance < random() then 261 | pos2.y = height 262 | objs = minetest.get_objects_inside_radius(pos2,radius*0.95) 263 | for _,obj in ipairs(objs) do -- do not spawn if another player around 264 | if obj:is_player() then return end 265 | end 266 | return pos2, liquidflag 267 | end 268 | end 269 | end 270 | end 271 | 272 | function mobkit.turn2yaw(self,tyaw,rate) 273 | tyaw = tyaw or 0 --temp 274 | rate = rate or 6 275 | local yaw = self.object:get_yaw() 276 | yaw = yaw+pi 277 | tyaw=(tyaw+pi)%(pi*2) 278 | 279 | local step=min(self.dtime*rate,abs(tyaw-yaw)%(pi*2)) 280 | 281 | local dir = abs(tyaw-yaw)>pi and -1 or 1 282 | dir = tyaw>yaw and dir*1 or dir * -1 283 | 284 | local nyaw = (yaw+step*dir)%(pi*2) 285 | self.object:set_yaw(nyaw-pi) 286 | 287 | if nyaw==tyaw then return true, nyaw-pi 288 | else return false, nyaw-pi end 289 | end 290 | 291 | function mobkit.dir_to_rot(v,rot) 292 | rot = rot or {x=0,y=0,z=0} 293 | 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})), 294 | y = (v.x==0 and v.z==0) and rot.y or minetest.dir_to_yaw(v), 295 | z=rot.z} 296 | end 297 | 298 | function mobkit.rot_to_dir(rot) -- keep rot within <-pi/2,pi/2> 299 | local dir = minetest.yaw_to_dir(rot.y) 300 | dir.y = dir.y+tan(rot.x)*vector.length(dir) 301 | return vector.normalize(dir) 302 | end 303 | 304 | function mobkit.isnear2d(p1,p2,thresh) 305 | if abs(p2.x-p1.x) < thresh and abs(p2.z-p1.z) < thresh then 306 | return true 307 | else 308 | return false 309 | end 310 | end 311 | 312 | -- object has reached the destination if dest is in the rear half plane. 313 | function mobkit.is_there_yet2d(pos,dir,dest) -- obj positon; facing vector; destination position 314 | 315 | local c = -dir.x*pos.x-dir.z*pos.z -- the constant 316 | 317 | if dir.z > 0 then 318 | return dest.z <= (-dir.x*dest.x - c)/dir.z -- line equation 319 | elseif dir.z < 0 then 320 | return dest.z >= (-dir.x*dest.x - c)/dir.z 321 | elseif dir.x > 0 then 322 | return dest.x <= (-dir.z*dest.z - c)/dir.x 323 | elseif dir.x < 0 then 324 | return dest.x >= (-dir.z*dest.z - c)/dir.x 325 | else 326 | return false 327 | end 328 | 329 | end 330 | 331 | function mobkit.isnear3d(p1,p2,thresh) 332 | if abs(p2.x-p1.x) < thresh and abs(p2.z-p1.z) < thresh and abs(p2.y-p1.y) < thresh then 333 | return true 334 | else 335 | return false 336 | end 337 | end 338 | 339 | function mobkit.get_box_intersect_cols(pos,box) 340 | local pmin = {x=floor(pos.x+box[1]+0.5),z=floor(pos.z+box[3]+0.5)} 341 | local pmax = {x=floor(pos.x+box[4]+0.5),z=floor(pos.z+box[6]+0.5)} 342 | 343 | result= {} 344 | for x=pmin.x,pmax.x do 345 | for z=pmin.z,pmax.z do 346 | table.insert(result,{x=x,z=z}) 347 | end 348 | end 349 | return result 350 | end 351 | 352 | function mobkit.get_box_displace_cols(pos,box,vec,dist) 353 | 354 | local result = {{}} 355 | -- front facing corner pos and neighbors 356 | local fpos = {pos.y} 357 | local xpos={pos.y} 358 | local zpos={pos.y} 359 | local xoff=nil 360 | local zoff=nil 361 | 362 | if vec.x < 0 then 363 | fpos.x = pos.x+box[1] -- frontmost corner's x 364 | xoff = box[4]-box[1] -- edge offset along x 365 | else 366 | fpos.x = pos.x+box[4] 367 | xoff = box[1]-box[4] 368 | end 369 | 370 | if vec.z < 0 then 371 | fpos.z = pos.z+box[3] -- frontmost corner's z 372 | zoff = box[6]-box[3] -- edge offset along z 373 | else 374 | fpos.z = pos.z+box[6] 375 | zoff = box[3]-box[6] 376 | end 377 | 378 | -- displacement vector 379 | if dist then vec = vector.multiply(vector.normalize(vec),dist) end 380 | 381 | -- traverse x 382 | local xsgn = sign(vec.x) 383 | local zsgn = sign(zoff) 384 | local index=0 385 | for x = floor(fpos.x+0.5)+xsgn*0.5, fpos.x+vec.x, xsgn do 386 | index=index+1 387 | if index > 50 then return result end 388 | result[index] = result[index] or {} 389 | local zcomp = vec.x == 0 and 0 or fpos.z + (x-fpos.x)*vec.z/vec.x -- z component at the intersection of x and node edge 390 | for z = floor(zcomp+0.5), floor(zcomp+zoff+0.5), zsgn do 391 | table.insert(result[index],{x=x+xsgn*0.5,z=z}) 392 | end 393 | end 394 | 395 | -- traverse z 396 | local zsgn = sign(vec.z) 397 | local xsgn = sign(xoff) 398 | index=0 399 | for z = floor(fpos.z + 0.5)+zsgn*0.5, fpos.z+vec.z, zsgn do 400 | index=index+1 401 | if index > 50 then return result end 402 | result[index] = result[index] or {} 403 | local xcomp = vec.z == 0 and 0 or fpos.x + (z-fpos.z)*vec.x/vec.z 404 | for x = floor(xcomp+0.5), floor(xcomp+xoff+0.5), xsgn do 405 | table.insert(result[index],{x=x,z=z+zsgn*0.5}) 406 | end 407 | end 408 | 409 | return result 410 | end 411 | 412 | function mobkit.get_box_height(thing) 413 | if type(thing) == 'table' then thing = thing.object end 414 | local colbox = thing:get_properties().collisionbox 415 | local height 416 | if colbox then height = colbox[5]-colbox[2] 417 | else height = 0.1 end 418 | 419 | return height > 0 and height or 0.1 420 | end 421 | 422 | function mobkit.is_alive(thing) -- thing can be luaentity or objectref. 423 | -- if not thing then return false end 424 | if not mobkit.exists(thing) then return false end 425 | if type(thing) == 'table' then return thing.hp > 0 end 426 | if thing:is_player() then return thing:get_hp() > 0 427 | else 428 | local lua = thing:get_luaentity() 429 | local hp = lua and lua.hp or nil 430 | return hp and hp > 0 431 | end 432 | end 433 | 434 | function mobkit.exists(thing) 435 | if not thing then return false end 436 | if type(thing) == 'table' then thing=thing.object end 437 | if type(thing) == 'userdata' then 438 | if thing:is_player() then 439 | if thing:get_look_horizontal() then return true end 440 | else 441 | if thing:get_yaw() then return true end 442 | end 443 | end 444 | end 445 | 446 | function mobkit.hurt(luaent,dmg) 447 | if not luaent then return false end 448 | if type(luaent) == 'table' then 449 | luaent.hp = max((luaent.hp or 0) - dmg,0) 450 | end 451 | end 452 | 453 | function mobkit.heal(luaent,dmg) 454 | if not luaent then return false end 455 | if type(luaent) == 'table' then 456 | luaent.hp = min(luaent.max_hp,(luaent.hp or 0) + dmg) 457 | end 458 | end 459 | 460 | function mobkit.animate(self,anim) 461 | if self.animation and self.animation[anim] then 462 | if self._anim == anim then return end 463 | self._anim=anim 464 | 465 | local aparms = {} 466 | if #self.animation[anim] > 0 then 467 | aparms = self.animation[anim][random(#self.animation[anim])] 468 | else 469 | aparms = self.animation[anim] 470 | end 471 | 472 | aparms.frame_blend = aparms.frame_blend or 0 473 | 474 | self.object:set_animation(aparms.range,aparms.speed,aparms.frame_blend,aparms.loop) 475 | else 476 | self._anim = nil 477 | end 478 | end 479 | 480 | function mobkit.make_sound(self, sound) 481 | local spec = self.sounds and self.sounds[sound] 482 | local param_table = {object=self.object} 483 | 484 | if type(spec) == 'table' then 485 | --pick random sound if it's a spec for random sounds 486 | if #spec > 0 then spec = spec[random(#spec)] end 487 | 488 | --returns value or a random value within the range [value[1], value[2]) 489 | local function in_range(value) 490 | return type(value) == 'table' and value[1]+random()*(value[2]-value[1]) or value 491 | end 492 | 493 | --pick random values within a range if they're a table 494 | param_table.gain = in_range(spec.gain) 495 | param_table.fade = in_range(spec.fade) 496 | param_table.pitch = in_range(spec.pitch) 497 | return minetest.sound_play(spec.name, param_table) 498 | end 499 | return minetest.sound_play(spec, param_table) 500 | end 501 | 502 | function mobkit.go_forward_horizontal(self,speed) -- sets velocity in yaw direction, y component unaffected 503 | local y = self.object:get_velocity().y 504 | local yaw = self.object:get_yaw() 505 | local vel = vector.multiply(minetest.yaw_to_dir(yaw),speed) 506 | vel.y = y 507 | self.object:set_velocity(vel) 508 | end 509 | 510 | function mobkit.drive_to_pos(self,tpos,speed,turn_rate,dist) 511 | local pos=self.object:get_pos() 512 | dist = dist or 0.2 513 | if mobkit.isnear2d(pos,tpos,dist) then return true end 514 | local tyaw = minetest.dir_to_yaw(vector.direction(pos,tpos)) 515 | mobkit.turn2yaw(self,tyaw,turn_rate) 516 | mobkit.go_forward_horizontal(self,speed) 517 | return false 518 | end 519 | 520 | function mobkit.timer(self,s) -- returns true approx every s seconds 521 | local t1 = floor(self.time_total) 522 | local t2 = floor(self.time_total+self.dtime) 523 | if t2>t1 and t2%s==0 then return true end 524 | end 525 | 526 | -- Memory functions. 527 | -- Stuff in memory is serialized, never try to remember objectrefs. 528 | function mobkit.remember(self,key,val) 529 | self.memory[key]=val 530 | return val 531 | end 532 | 533 | function mobkit.forget(self,key) 534 | self.memory[key] = nil 535 | end 536 | 537 | function mobkit.recall(self,key) 538 | return self.memory[key] 539 | end 540 | 541 | -- Queue functions 542 | function mobkit.queue_high(self,func,priority) 543 | local maxprty = mobkit.get_queue_priority(self) 544 | if priority > maxprty then 545 | mobkit.clear_queue_low(self) 546 | end 547 | 548 | for i,f in ipairs(self.hqueue) do 549 | if priority > f.prty then 550 | table.insert(self.hqueue,i,{func=func,prty=priority}) 551 | return 552 | end 553 | end 554 | table.insert(self.hqueue,{func=func,prty=priority}) 555 | end 556 | 557 | function mobkit.queue_low(self,func) 558 | table.insert(self.lqueue,func) 559 | end 560 | 561 | function mobkit.is_queue_empty_low(self) 562 | if #self.lqueue == 0 then return true 563 | else return false end 564 | end 565 | 566 | function mobkit.clear_queue_high(self) 567 | self.hqueue = {} 568 | end 569 | 570 | function mobkit.clear_queue_low(self) 571 | self.lqueue = {} 572 | end 573 | 574 | function mobkit.get_queue_priority(self) 575 | if #self.hqueue > 0 then 576 | return self.hqueue[1].prty 577 | else return 0 end 578 | end 579 | 580 | function mobkit.is_queue_empty_high(self) 581 | if #self.hqueue == 0 then return true 582 | else return false end 583 | end 584 | 585 | function mobkit.get_nearby_player(self) -- returns random player if nearby or nil 586 | for _,obj in ipairs(self.nearby_objects) do 587 | if obj:is_player() and mobkit.is_alive(obj) then return obj end 588 | end 589 | return 590 | end 591 | 592 | function mobkit.get_nearby_entity(self,name) -- returns random nearby entity of name or nil 593 | for _,obj in ipairs(self.nearby_objects) do 594 | if mobkit.is_alive(obj) and not obj:is_player() and obj:get_luaentity().name == name then return obj end 595 | end 596 | return 597 | end 598 | 599 | function mobkit.get_closest_entity(self,name) -- returns closest entity of name or nil 600 | local cobj = nil 601 | local dist = abr*64 602 | local pos = self.object:get_pos() 603 | for _,obj in ipairs(self.nearby_objects) do 604 | local luaent = obj:get_luaentity() 605 | if mobkit.is_alive(obj) and not obj:is_player() and luaent and luaent.name == name then 606 | local opos = obj:get_pos() 607 | local odist = abs(opos.x-pos.x) + abs(opos.z-pos.z) 608 | if odist < dist then 609 | dist=odist 610 | cobj=obj 611 | end 612 | end 613 | end 614 | return cobj 615 | end 616 | 617 | local function execute_queues(self) 618 | --Execute hqueue 619 | if #self.hqueue > 0 then 620 | local func = self.hqueue[1].func 621 | if func(self) then 622 | table.remove(self.hqueue,1) 623 | self.lqueue = {} 624 | end 625 | end 626 | -- Execute lqueue 627 | if #self.lqueue > 0 then 628 | local func = self.lqueue[1] 629 | if func(self) then 630 | table.remove(self.lqueue,1) 631 | end 632 | end 633 | end 634 | 635 | local function sensors() 636 | local timer = 2 637 | local pulse = 1 638 | return function(self) 639 | timer=timer-self.dtime 640 | if timer < 0 then 641 | 642 | pulse = pulse + 1 -- do full range every third scan 643 | local range = self.view_range 644 | if pulse > 2 then 645 | pulse = 1 646 | else 647 | range = self.view_range*0.5 648 | end 649 | 650 | local pos = self.object:get_pos() 651 | --local tim = minetest.get_us_time() 652 | self.nearby_objects = minetest.get_objects_inside_radius(pos, range) 653 | --minetest.chat_send_all(minetest.get_us_time()-tim) 654 | for i,obj in ipairs(self.nearby_objects) do 655 | if obj == self.object then 656 | table.remove(self.nearby_objects,i) 657 | break 658 | end 659 | end 660 | timer=2 661 | end 662 | end 663 | end 664 | 665 | ------------ 666 | -- CALLBACKS 667 | ------------ 668 | 669 | function mobkit.default_brain(self) 670 | if mobkit.is_queue_empty_high(self) then mobkit.hq_roam(self,0) end 671 | end 672 | 673 | function mobkit.physics(self) 674 | local vel=self.object:get_velocity() 675 | local vnew = vector.new(vel) 676 | -- dumb friction 677 | 678 | if self.isonground and not self.isinliquid then 679 | vnew = {x= vel.x> 0.2 and vel.x*mobkit.friction or 0, 680 | y=vel.y, 681 | z=vel.z > 0.2 and vel.z*mobkit.friction or 0} 682 | end 683 | 684 | -- bounciness 685 | if self.springiness and self.springiness > 0 then 686 | 687 | if colinfo and colinfo.collides then 688 | for _,c in ipairs(colinfo.collisions) do 689 | if c.old_velocity[c.axis] > 0.1 then 690 | vnew[c.axis] = vnew[c.axis] * self.springiness * -1 691 | end 692 | end 693 | elseif not colinfo then -- MT 5.2 and earlier 694 | for _,k in ipairs({'y','z','x'}) do 695 | if vel[k]==0 and abs(self.lastvelocity[k])> 0.1 then 696 | vnew[k]=-self.lastvelocity[k]*self.springiness 697 | end 698 | end 699 | end 700 | end 701 | 702 | self.object:set_velocity(vnew) 703 | 704 | -- buoyancy 705 | local surface = nil 706 | local surfnodename = nil 707 | local spos = mobkit.get_stand_pos(self) 708 | spos.y = spos.y+0.01 709 | -- get surface height 710 | local snodepos = mobkit.get_node_pos(spos) 711 | local surfnode = mobkit.nodeatpos(spos) 712 | while surfnode and surfnode.drawtype == 'liquid' do 713 | surfnodename = surfnode.name 714 | surface = snodepos.y+0.5 715 | if surface > spos.y+self.height then break end 716 | snodepos.y = snodepos.y+1 717 | surfnode = mobkit.nodeatpos(snodepos) 718 | end 719 | self.isinliquid = surfnodename 720 | if surface then -- standing in liquid 721 | -- self.isinliquid = true 722 | local submergence = min(surface-spos.y,self.height)/self.height 723 | -- local balance = self.buoyancy*self.height 724 | local buoyacc = mobkit.gravity*(self.buoyancy-submergence) 725 | mobkit.set_acceleration(self.object, 726 | {x=-vel.x*self.water_drag,y=buoyacc-vel.y*abs(vel.y)*0.4,z=-vel.z*self.water_drag}) 727 | else 728 | -- self.isinliquid = false 729 | self.object:set_acceleration({x=0,y=mobkit.gravity,z=0}) 730 | end 731 | 732 | end 733 | 734 | function mobkit.vitals(self) 735 | -- vitals: fall damage 736 | local vel = self.object:get_velocity() 737 | local velocity_delta = abs(self.lastvelocity.y - vel.y) 738 | if velocity_delta > mobkit.safe_velocity then 739 | self.hp = self.hp - floor(self.max_hp * min(1, velocity_delta/mobkit.terminal_velocity)) 740 | end 741 | 742 | -- vitals: oxygen 743 | if self.lung_capacity then 744 | local colbox = self.object:get_properties().collisionbox 745 | local headnode = mobkit.nodeatpos(mobkit.pos_shift(self.object:get_pos(),{y=colbox[5]})) -- node at hitbox top 746 | if headnode and headnode.drawtype == 'liquid' then 747 | self.oxygen = self.oxygen - self.dtime 748 | else 749 | self.oxygen = self.lung_capacity 750 | end 751 | 752 | if self.oxygen <= 0 then self.hp=0 end -- drown 753 | end 754 | end 755 | 756 | function mobkit.statfunc(self) 757 | local tmptab={} 758 | tmptab.memory = self.memory 759 | tmptab.hp = self.hp 760 | tmptab.texture_no = self.texture_no 761 | return minetest.serialize(tmptab) 762 | end 763 | 764 | function mobkit.actfunc(self, staticdata, dtime_s) 765 | 766 | self.logic = self.logic or self.brainfunc 767 | self.physics = self.physics or mobkit.physics 768 | 769 | self.lqueue = {} 770 | self.hqueue = {} 771 | self.nearby_objects = {} 772 | self.nearby_players = {} 773 | self.pos_history = {} 774 | self.path_dir = 1 775 | self.time_total = 0 776 | self.water_drag = self.water_drag or 1 777 | 778 | local sdata = minetest.deserialize(staticdata) 779 | if sdata then 780 | for k,v in pairs(sdata) do 781 | self[k] = v 782 | end 783 | end 784 | 785 | if self.textures==nil then 786 | local prop_tex = self.object:get_properties().textures 787 | if prop_tex then self.textures=prop_tex end 788 | end 789 | 790 | if not self.memory then -- this is the initial activation 791 | self.memory = {} 792 | 793 | -- texture variation 794 | if #self.textures > 1 then self.texture_no = random(#self.textures) end 795 | end 796 | 797 | if self.timeout and ((self.timeout>0 and dtime_s > self.timeout and next(self.memory)==nil) or 798 | (self.timeout<0 and dtime_s > abs(self.timeout))) then 799 | self.object:remove() 800 | end 801 | 802 | -- apply texture 803 | if self.textures and self.texture_no then 804 | local props = {} 805 | props.textures = {self.textures[self.texture_no]} 806 | self.object:set_properties(props) 807 | end 808 | 809 | --hp 810 | self.max_hp = self.max_hp or 10 811 | self.hp = self.hp or self.max_hp 812 | --armor 813 | if type(self.armor_groups) ~= 'table' then 814 | self.armor_groups={} 815 | end 816 | self.armor_groups.immortal = 1 817 | self.object:set_armor_groups(self.armor_groups) 818 | 819 | self.buoyancy = self.buoyancy or 0 820 | self.oxygen = self.oxygen or self.lung_capacity 821 | self.lastvelocity = {x=0,y=0,z=0} 822 | self.sensefunc=sensors() 823 | end 824 | 825 | function mobkit.stepfunc(self,dtime,colinfo) -- not intended to be modified 826 | self.dtime = min(dtime,0.2) 827 | self.colinfo = colinfo 828 | self.height = mobkit.get_box_height(self) 829 | 830 | -- physics comes first 831 | local vel = self.object:get_velocity() 832 | 833 | if colinfo then 834 | self.isonground = colinfo.touching_ground 835 | else 836 | if self.lastvelocity.y==0 and vel.y==0 then 837 | self.isonground = true 838 | else 839 | self.isonground = false 840 | end 841 | end 842 | 843 | self:physics() 844 | 845 | if self.logic then 846 | if self.view_range then self:sensefunc() end 847 | self:logic() 848 | execute_queues(self) 849 | end 850 | 851 | self.lastvelocity = self.object:get_velocity() 852 | self.time_total=self.time_total+self.dtime 853 | end 854 | 855 | -- load example behaviors 856 | dofile(minetest.get_modpath("mobkit") .. "/example_behaviors.lua") 857 | 858 | minetest.register_on_mods_loaded(function() 859 | local mbkfuns = '' 860 | for n,f in pairs(mobkit) do 861 | if type(f) == 'function' then 862 | mbkfuns = mbkfuns .. n .. string.split(minetest.serialize(f),'.lua')[2] or '' 863 | end 864 | end 865 | local crc = minetest.sha1(mbkfuns) 866 | -- dbg(crc) 867 | -- if crc ~= 'a061770008fe9ecf8e1042a227dc3beabd10e481' then 868 | -- minetest.log("error","Mobkit namespace inconsistent, has been modified by other mods.") 869 | -- end 870 | end) 871 | -------------------------------------------------------------------------------- /mobkit_api.txt: -------------------------------------------------------------------------------- 1 | Contents 2 | 3 | 1 Concepts 4 | 1.1 Behavior functions 5 | 1.1.1 Low level functions 6 | 1.1.2 High level functions 7 | 1.1.2.1 Priority 8 | 1.1.3 Modifying built in behaviors 9 | 1.2 Logic function 10 | 1.3 Processing diagram 11 | 1.4 Entity definition 12 | 1.5 Exposed luaentity members 13 | 14 | 2 Reference 15 | 2.1 Utility functions 16 | 2.2 Built in behaviors 17 | 2.2.1 High level behaviors 18 | 2.2.2 Low level behaviors 19 | 2.3 Constants and member variables 20 | 21 | ----------- 22 | 1. Concepts 23 | ----------- 24 | 25 | 1.1 Behavior functions 26 | 27 | These are the most fundamental units of code, every action entities can perform is a separate function. 28 | There are two types of behaviors: 29 | - low level, these govern physical actions and interactions (think moves) 30 | - high level, these are logical structures governing low level behaviors in order to perform more complex tasks 31 | 32 | Behaviors run for considerable amount of time, this means the functions are being called repeatedly on consecutive engine steps. 33 | Therefore a need for preserving state between calls, this is why they are implemented as closures, see defining conventions for details. 34 | 35 | Behavior functions are active until they finish the job, are removed from the queue or superseded by a higher priority behavior. 36 | They signal finished state by returning true, therefore it's very important to carefully design the completion conditions 37 | 38 | 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. 39 | Queuing is covered by behavour defining conventions 40 | 41 | Mobkit comes with some example behavior functions, which are located in /example_behaviors.lua 42 | !!! In simplest scenarios there's no need to code behaviors, much can be achieved using only built-in stuff !!! 43 | !!! To start using the api it's enough to learn defining mobs and writing brain functions !!! 44 | 45 | 46 | 1.1.1 Low level behavior functions 47 | 48 | These are physical actions and interactions: steps, jumps, turns etc. here you'll set velocity, yaw, kick off animations and sounds. 49 | 50 | Low level behavior definition: 51 | 52 | function mobkit.lq_bhv1(self,[optional additional persistent parameters]) -- enclosing function 53 | ... -- optional definitions of additional persistent variables 54 | local func=function(self) -- enclosed function, self is mandatory and the only allowed parameter 55 | ... -- actual function definition, remember to return true eventually 56 | end 57 | mobkit.queue_low(self,func) -- this will queue the behavior at the time of lq_bhv1 call 58 | end 59 | 60 | 61 | 1.1.2 High level behavior functions 62 | 63 | These are complex tasks like getting to a position, following other objects, hiding, patrolling an area etc. 64 | Their job is tracking changes in the environment and managing low level behavior queue accordingly. 65 | 66 | High level behavior definition: 67 | 68 | function mobkit.hq_bhv1(self,priority,[optional additional persistent parameters]) -- enclosing function 69 | ... -- optional definitions of additional persistent variables 70 | local func=function(self) -- enclosed function, self is mandatory and the only allowed parameter 71 | ... -- actual function definition, remember to return true eventually 72 | end 73 | mobkit.queue_high(self,func,priority) -- this will queue the behavior at the time of hq_bhv1 call 74 | end 75 | 76 | 77 | 1.1.2.1 Priority 78 | 79 | Unlike low level behaviors which are executed in FIFO order, high level behaviors support prioritization. 80 | This concept is essential for making sure the right behavior is active at the right time. 81 | Prioritization is what makes it possible to interrupt a task in order to perform a more important one 82 | 83 | The currently executing behavior is always the first in the queue. 84 | When a new behavior is placed onto the queue: 85 | If the queue is not empty a new behavior is inserted before the first behavior of lower priority if such exists, or last. 86 | If the new behavior supersedes the one currently executing, low level queue is purged immediately. 87 | 88 | Common idioms: 89 | 90 | hq_bhv1(self,prty): 91 | ... 92 | hq_bhv2(self,prty) -- bhv1 kicks off bhv2 with equal priority 93 | return true -- and ends, 94 | -- bhv2 becomes active on the next engine step. 95 | 96 | hq_bhv1(self,prty): 97 | ... 98 | hq_bhv2(self,prty+1) -- bhv1 kicks off bhv2 with higher priority 99 | -- bhv2 takes over and when it ends, bhv1 resumes. 100 | 101 | 102 | Particular prioritization scheme is to be designed by the user according to specific mod requirements. 103 | 104 | 1.1.3 Modifying built in behaviors 105 | 106 | Do not modify example_behaviors.lua directly, because functions defined there are meant to be shared between mods. 107 | Instead, copy the contents of /behaviors2override.lua into your mod/game, changing every occurence of the string '[yournamespace]' to the name of a lua table representing your namespace of choice. 108 | 109 | 1.2 Logic function 110 | ------------------ 111 | Every mob must have one. 112 | Its job is managing high level behavior queue in response to events which are not intercepted by callbacks. 113 | Contrary to what the name suggests, these functions needn't necessarily be too complex thanks to their limited responsibilities. 114 | 115 | Typical flow might look like this: 116 | 117 | if mobkit.timer(self,1) then -- returns true approx every second 118 | local prty = mobkit.get_queue_priority(self) 119 | 120 | if prty < 20 121 | if ... then 122 | hq_do_important_stuff(self,20) 123 | return 124 | end 125 | end 126 | 127 | if prty < 10 then 128 | if ... then 129 | hq_do_something_else(self,10) 130 | return 131 | elseif ... then 132 | hq_do_this_instead(self,10) 133 | return 134 | end 135 | end 136 | 137 | if mobkit.is_queue_empty_high(self) then 138 | hq_fool_around(self,0) 139 | end 140 | end 141 | 142 | 143 | 1.3 Processing diagram 144 | ---------------------- 145 | 146 | --------------------------------------- 147 | | PHYSICS | 148 | | | 149 | | ----------------------- | 150 | | | Logic Function | | 151 | | ----------------------- | 152 | | | | 153 | | -----|----------------- | 154 | | | V HL Queue| | 155 | | | 1| 2| 3|... | | 156 | | ----------------------- | 157 | | | | 158 | | -----|----------------- | 159 | | | V LL Queue| | 160 | | | 1| 2| 3|... | | 161 | | ----------------------- | 162 | | | 163 | --------------------------------------- 164 | 165 | Order of execution during an engine step: 166 | First comes physics: gravity, buoyancy, friction etc., then the logic function is called. 167 | After that, the first behavior on the high level queue, if exists, 168 | and the last, the first low level behavior if present. 169 | 170 | 1.4 Entity definition 171 | --------------------- 172 | 173 | minetest.register_entity("mod:name",{ 174 | 175 | -- required minetest api props 176 | 177 | initial_properties = { 178 | physical = true, 179 | collide_with_objects = true, 180 | collisionbox = {...}, 181 | visual = "mesh", 182 | mesh = "...", 183 | textures = {...}, 184 | }, 185 | 186 | 187 | -- required mobkit props 188 | 189 | timeout = [num], -- entities are removed after this many seconds inactive 190 | -- 0 is never 191 | -- mobs having memory entries are not affected 192 | 193 | buoyancy = [num], -- (0,1) - portion of collisionbox submerged 194 | -- = 1 - controlled buoyancy (fish, submarine) 195 | -- > 1 - drowns 196 | -- < 0 - MC like water trampolining 197 | 198 | lung_capacity = [num], -- seconds 199 | max_hp = [num], 200 | on_step = mobkit.stepfunc, 201 | on_activate = mobkit.actfunc, 202 | get_staticdata = mobkit.statfunc, 203 | logic = [function user defined], -- older 'brainfunc' name works as well. 204 | 205 | -- optional mobkit props 206 | -- or used by built in behaviors 207 | physics = [function user defined] -- optional, overrides built in physics 208 | animation = { 209 | [name]={range={x=[num],y=[num]},speed=[num],loop=[bool]}, -- single 210 | 211 | [name]={ -- variant, animations are chosen randomly. 212 | {range={x=[num],y=[num]},speed=[num],loop=[bool]}, 213 | {range={x=[num],y=[num]},speed=[num],loop=[bool]}, 214 | ... 215 | } 216 | ... 217 | } 218 | sounds = { 219 | [name] = [string filename], --single, simple, 220 | 221 | [name] = { --single, more powerful. All fields but 'name' are optional 222 | name = [string filename], 223 | gain=[num or range], --range is a table of the format {left_bound, right_bound} 224 | fade=[num or range], 225 | pitch=[num or range], 226 | }, 227 | 228 | [name] = { --variant, sound is chosen randomly 229 | { 230 | name = [string filename], 231 | gain=[num or range], 232 | fade=[num or range], 233 | pitch=[num or range], 234 | }, 235 | { 236 | name = [string filename], 237 | gain=[num or range], 238 | fade=[num or range], 239 | pitch=[num or range], 240 | }, 241 | ... 242 | }, 243 | ... 244 | }, 245 | max_speed = [num], -- m/s 246 | jump_height = [num], -- nodes/meters 247 | view_range = [num], -- nodes/meters 248 | attack={range=[num], -- range is distance between attacker's collision box center 249 | damage_groups={fleshy=[num]}}, -- and the tip of the murder weapon in nodes/meters 250 | armor_groups = {fleshy=[num]} 251 | }) 252 | 253 | 1.5 Exposed luaentity members 254 | 255 | Some frequently used entity fields to be accessed directly for convenience 256 | 257 | self.dtime -- max(dtime as passed to on_step,0.5) - limit of 0.05 to prevent jerkines on long steps. 258 | self.hp -- hitpoints 259 | self.isonground -- true if in collision with negative Y 260 | self.isinliquid -- true if the node at foot level is drawtype=='liquid' 261 | 262 | ------------ 263 | 2. Reference 264 | ------------ 265 | 266 | 2.1 Utility Functions 267 | 268 | function mobkit.minmax(v,m) 269 | -- v,n: numbers 270 | -- returns v trimmed to <-m,m> range 271 | 272 | function mobkit.get_terrain_height(pos,steps) 273 | -- recursively search for walkable surface at pos. 274 | -- steps (optional) is how far from pos it gives up, expressed in nodes, default 3 275 | -- Returns: 276 | -- surface height at pos, or nil if not found 277 | -- liquid flag: true if found surface is covered with liquid 278 | 279 | function mobkit.turn2yaw(self,tyaw,rate) 280 | -- gradually turns towards yaw 281 | -- self: luaentity 282 | -- tyaw: target yaw in radians 283 | -- rate: turn rate in rads/s 284 | --returns: true if facing tyaw; current yaw 285 | 286 | function mobkit.timer(self,s) 287 | -- returns true approx every s seconds 288 | -- used to reduce execution of code that needn't necessarily be done on every engine step 289 | 290 | function mobkit.pos_shift(pos,vec) 291 | -- convenience function 292 | -- returns pos shifted by vec 293 | -- vec needn't have all three components given, absent components are assumed zero. 294 | -- e.g pos_shift(pos,{y=1}) is valid 295 | 296 | function mobkit.pos_translate2d(pos,yaw,dist) 297 | -- returns pos translated in the yaw direction by dist 298 | 299 | function mobkit.get_stand_pos(thing) 300 | -- returns object pos projected onto the bottom collisionbox face 301 | -- thing can be luaentity or objectref. 302 | 303 | function mobkit.nodeatpos(pos) 304 | -- convenience function 305 | -- returns nodedef or nil if it's an ignore node 306 | 307 | function mobkit.get_node_pos(pos) 308 | -- returns center of the node that pos is inside 309 | 310 | function mobkit.get_nodes_in_area(pos1,pos2,[full]) 311 | -- in basic mode returns a table of unique nodes within area indexed by node 312 | -- in full=true mode returns a table of nodes indexed by pos 313 | -- works for up to 125 nodes. 314 | 315 | function mobkit.isnear2d(p1,p2,thresh) 316 | -- returns true if pos p2 is within a square with center at pos p1 and radius thresh 317 | -- y components are ignored 318 | 319 | function mobkit.is_there_yet2d(pos,dir,dest) -- obj positon; facing vector; destination position 320 | -- returns true if a position dest is behind position pos according to facing vector dir 321 | -- (checks if dest is in the rear half plane as defined by pos and dir) 322 | -- y components are ignored 323 | 324 | function mobkit.isnear3d(p1,p2,thresh) 325 | -- returns true if pos p2 is within a cube with center at pos p1 and radius thresh 326 | 327 | function mobkit.get_box_intersect_cols(pos,box) 328 | -- returns an array of {x=,z=} columns that the box intersects with. 329 | 330 | function mobkit.get_box_displace_cols(pos,box,vec,dist) 331 | -- returns an array of {x=,z=} columns that the box would pass by if moved by horizontal vector vec 332 | -- if dist provided, vec gets normalized. 333 | 334 | function mobkit.dir_to_rot(v,rot) 335 | -- converts a 3d vector v to rotation like in set_rotation() object method 336 | -- rot (optional) is current object rotation 337 | 338 | function mobkit.rot_to_dir(rot) 339 | -- converts minetest rotation vector (pitch,yaw,roll) to direction unit vector 340 | 341 | function mobkit.is_alive(thing) 342 | -- non essential, checks if thing exists in the world and is alive 343 | -- makes an assumption that luaentities are considered dead when their hp < 100 344 | -- thing can be luaentity or objectref. 345 | -- used for stored luaentities and objectrefs 346 | 347 | function mobkit.exists(thing) 348 | -- checks if thing exists in the world 349 | -- thing can be luaentity or objectref. 350 | -- used for stored luaentities and objectrefs 351 | 352 | function mobkit.hurt(luaent,dmg) 353 | -- decrease luaent.hp by dmg 354 | 355 | function mobkit.heal(luaent,dmg) 356 | -- increase luaent.hp by dmg 357 | 358 | function mobkit.get_spawn_pos_abr(dtime,intrvl,radius,chance,reduction) 359 | -- returns a potential spawn position at random intervals 360 | -- intrvl: avg spawn attempt interval for every player 361 | -- radius: spawn distance in nodes, active_block_range*16 is recommended 362 | -- chance: (0,1) chance to spawn a mob if there are no other objects in area 363 | -- reduction: (0,1) spawn chance is reduced by this factor for every object in range. 364 | --usage: 365 | minetest.register_globalstep(function(dtime) 366 | local spawnpos = mobkit.get_spawn_pos_abr(...) 367 | if spawnpos then 368 | ... -- mod/game specific logic 369 | end 370 | end) 371 | 372 | function mobkit.animate(self,anim) 373 | -- makes an entity play an animation of name anim, or does nothing if not defined 374 | -- anim is string, see entity definition 375 | -- does nothing if the same animation is already running 376 | 377 | function mobkit.make_sound(self,sound) 378 | -- sound is string, see entity definition 379 | -- makes an entity play sound, or does nothing if not defined 380 | --returns sound handle 381 | 382 | function mobkit.go_forward_horizontal(self,speed) 383 | -- sets an entity's horizontal velocity in yaw direction. Vertical velocity unaffected. 384 | 385 | function mobkit.drive_to_pos(self,tpos,speed,turn_rate,dist) 386 | -- moves in facing direction while gradually turning towards tpos, returns true if in dist distance from tpos 387 | -- tpos: target position 388 | -- speed: in m/s 389 | -- turn_rate: in rad/s 390 | -- dist: in m. 391 | 392 | -- Memory functions. 393 | 394 | This represents mob long term memory 395 | Warning: Stuff in memory is serialized, never try to remember objectrefs or tables referencing them 396 | or the engine will crash. 397 | 398 | function mobkit.remember(self,key,val) 399 | -- premanently store a key, value pair 400 | function mobkit.forget(self,key) 401 | -- clears a memory entry 402 | function mobkit.recall(self,key) 403 | -- returns val associated with key 404 | 405 | -- Queue functions 406 | 407 | function mobkit.queue_high(self,func,priority) 408 | -- only for use in behavior definitions, see 1.1.2 409 | 410 | function mobkit.queue_low(self,func) 411 | -- only for use in behavior definitions, see 1.1.1 412 | 413 | 414 | function mobkit.clear_queue_high(self) 415 | function mobkit.clear_queue_low(self) 416 | 417 | function mobkit.is_queue_empty_high(self) 418 | function mobkit.is_queue_empty_low(self) 419 | 420 | function mobkit.get_queue_priority(self) 421 | -- returns the priority of currently running behavior 422 | -- this is also the highest of all queued behaviors 423 | 424 | 425 | -- Use these inside logic functions -- 426 | 427 | function mobkit.vitals(self) 428 | -- default drowning and fall damage, call it before hp check 429 | function mobkit.get_nearby_player(self) 430 | -- returns random player if nearby or nil 431 | function mobkit.get_nearby_entity(self,name) 432 | -- returns random nearby entity of name or nil 433 | function mobkit.get_closest_entity(self,name) 434 | -- returns closest entity of name or nil 435 | 436 | 437 | -- Misc 438 | 439 | Neighbors structure represents a node's horizontal neighbors 440 | Not essential, used by some built in behaviors 441 | Custom behaviors may not need it. 442 | 443 | Neighbor #1 is offset {x=1,z=0}, subsequent numbers go clockwise 444 | 445 | function mobkit.dir2neighbor(dir) 446 | -- converts a 3d vector to neighbor number, y component ignored 447 | 448 | function mobkit.neighbor_shift(neighbor,shift) 449 | -- get another neighbor number relative to the given, shift: plus is clockwise, minus the opposite 450 | -- 1,1 = 2; 1,-2 = 7 451 | 452 | 453 | 2.2 Built in behaviors 454 | 455 | function mobkit.goto_next_waypoint(self,tpos) 456 | -- this functions groups common operations making mobs move in a specific direction 457 | -- not a behavior itself, but is used by some built in HL behaviors 458 | -- which use node by node movement algorithm 459 | 460 | 2.2.1 High Level Behaviors -- 461 | 462 | function mobkit.hq_roam(self,prty) 463 | -- slow random roaming 464 | -- never returns 465 | 466 | function mobkit.hq_follow(self,prty,tgtobj) 467 | -- follow the tgtobj 468 | -- returns if tgtobj becomes inactive 469 | 470 | function mobkit.hq_goto(self,prty,tpos) 471 | -- go to tpos position 472 | -- returns on arrival 473 | 474 | function mobkit.hq_runfrom(self,prty,tgtobj) 475 | -- run away from tgtobj object 476 | -- returns when tgtobj far enough 477 | 478 | function mobkit.hq_hunt(self,prty,tgtobj) 479 | -- follow tgtobj and when close enough, kick off hq_attack 480 | -- returns when tgtobj too far 481 | 482 | function mobkit.hq_warn(self,prty,tgtobj) 483 | -- when a tgtobj close by, turn towards them and make the 'warn' sound 484 | -- kick off hq_hunt if tgtobj too close or timer expired 485 | -- returns when tgtobj moves away 486 | 487 | function mobkit.hq_die(self) 488 | -- default death, rotate and remove() after set time 489 | 490 | function mobkit.hq_attack(self,prty,tgtobj) 491 | -- default attack, turns towards tgtobj and leaps 492 | -- returns when tgtobj out of range 493 | 494 | function mobkit.hq_liquid_recovery(self,prty) 495 | -- use when submerged in liquid, scan for nearest land 496 | -- if land is found within view_range, kick off hq_swimto 497 | -- otherwise die 498 | 499 | function mobkit.hq_swimto(self,prty,tpos) 500 | -- swim towards the position tpos, jump if necessary 501 | -- returns if standing firmly on dry land 502 | 503 | Aquatic behaviors: 504 | 505 | Macros: 506 | function aqua_radar_dumb(pos,yaw,range,reverse) 507 | -- assumes a mob will avoid shallows 508 | -- checks if a pos in front of a moving entity swimmable 509 | -- otherwise returns new position 510 | 511 | function mobkit.is_in_deep(target) 512 | -- checks if an object is in water at least 2 nodes deep 513 | 514 | Hq Behaviors: 515 | function mobkit.hq_aqua_roam(self,prty,speed) 516 | function mobkit.hq_aqua_attack(self,prty,tgtobj,speed) 517 | function mobkit.hq_aqua_turn(self,prty,tyaw,speed) 518 | -- used by both previous bhv 519 | 520 | 2.2.2 Low Level Behaviors -- 521 | 522 | function mobkit.lq_turn2pos(self,tpos) 523 | -- gradually turn towards tpos position 524 | -- returns when facing tpos 525 | 526 | function mobkit.lq_idle(self,duration) 527 | -- do nothing for duration seconds 528 | -- set 'stand' animation 529 | 530 | function mobkit.lq_dumbwalk(self,dest,speed_factor) 531 | -- simply move towards dest 532 | -- set 'walk' animation 533 | 534 | function mobkit.lq_dumbjump(self,height) 535 | -- if standing on the ground, jump in the facing direction 536 | -- height is relative to feet level 537 | -- set 'stand' animation 538 | 539 | function mobkit.lq_freejump(self) 540 | -- unconditional jump in the facing direction 541 | -- useful e.g for getting out of water 542 | -- returns when the apex has been reached 543 | 544 | function mobkit.lq_jumpattack(self,height,target) 545 | -- jump towards the target, punch if a hit 546 | -- returns after punch or on the ground 547 | 548 | function mobkit.lq_fallover(self) 549 | -- gradually rotates Z = 0 to pi/2 550 | 551 | 552 | 2.3 Constants and member variables -- 553 | 554 | mobkit.gravity = -9.8 555 | mobkit.friction = 0.4 -- inert entities will slow down when in contact with the ground 556 | -- the smaller the number, the greater the effect 557 | 558 | self.dtime -- for convenience, dtime as passed to currently executing on_step() 559 | self.isonground -- true if y velocity is 0 for at least two succesive steps 560 | self.isinliquid -- true if feet submerged in liquid type=source 561 | -------------------------------------------------------------------------------- /mod.conf: -------------------------------------------------------------------------------- 1 | name = mobkit 2 | description = Entity API 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheTermos/mobkit/ddea141b081e087900a6acc5a2a90e8d4e564295/screenshot.png --------------------------------------------------------------------------------