mob.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. local get_relevant_players = limbo_respawning.get_living_players
  2. local vector_mul = vector.multiply
  3. local vector_add = vector.add
  4. local vector_dist = vector.distance
  5. local raycast = minetest.raycast
  6. local get_node = minetest.get_node
  7. local mobs = defense_mob_api
  8. mobs.gravity = -9.81
  9. mobs.default_prototype = {
  10. -- minetest properties & defaults
  11. physical = true,
  12. collide_with_objects = true,
  13. makes_footstep_sound = true,
  14. visual = "mesh",
  15. automatic_face_movement_dir = true,
  16. stepheight = 0.6,
  17. -- custom properties
  18. id = 0, -- Automatically set
  19. smart_path = true, -- Use the pathfinder
  20. --organize in a swarm
  21. swarm = false,
  22. --for switching pathfinding on and off depending on line of line_of_sight
  23. --if negative (default) it's always on, if >= 0 it'll switch
  24. smart_path_for = -1,
  25. mass = 1,
  26. movement = "ground", -- "ground"/"air"
  27. move_speed = 1,
  28. jump_height = 1,
  29. armor = 0,
  30. attack_range = 1,
  31. attack_damage = 1,
  32. attack_interval = 1,
  33. current_animation = nil,
  34. current_animation_end = 0,
  35. destination = nil, -- position
  36. last_attack_time = 0,
  37. life_timer = 75,
  38. pause_timer = 0,
  39. timer = 0,
  40. media_prefix = "modname_mobname_", --set properly in register_mob
  41. idle_sound_cd = 5,
  42. time_since_last_idle_sound = 0,
  43. view_range = 20,
  44. -- cache
  45. cache_is_standing = nil,
  46. cache_find_nearest_player = nil,
  47. }
  48. local reg_nodes = minetest.registered_nodes
  49. local function vec_zero() return {x=0, y=0, z=0} end
  50. do --look
  51. function mobs.default_prototype:look(dir)
  52. local pos = self.object:get_pos()
  53. local dest = vector_add(pos, vector_mul(dir, self.view_range))
  54. local ray = raycast(pos, dest, true, true)
  55. local b = true
  56. while b
  57. do
  58. b = false
  59. local pt = ray:next()
  60. if pt
  61. then
  62. if pt.type == node
  63. then
  64. local node = get_node(pt.under)
  65. if node and node.name ~= "ignore"
  66. then
  67. if not(reg_nodes[node.name] or {walkable = true}).walkable
  68. then
  69. b = true
  70. end
  71. end
  72. if not b
  73. then
  74. return false, pt.under
  75. end
  76. end
  77. --[[elseif pt.type == object and
  78. vector_dist(pt.intersection_point, pos) > 2
  79. then
  80. return false
  81. end]]
  82. end
  83. end
  84. return true
  85. end
  86. end
  87. function mobs.default_prototype:make_sound(event_name, detached, gain, pitch, max_hear_dist)
  88. local def =
  89. {
  90. object = self.object,
  91. gain = gain or 0.5 + math.random() * 0.5,
  92. pitch = pitch or 1 + (math.random() - 0.5) * 0.5,
  93. max_hear_distance = max_hear_dist,
  94. }
  95. if detached then def.object = nil end
  96. return minetest.sound_play(self.media_prefix .. event_name, def)
  97. end
  98. --on_activate
  99. do--by using local scopes we can re-use the names for observer storage tables
  100. local observers, ocount = {}, 0
  101. --for registering functions as callback
  102. mobs.register_on_default_activate = function (func)
  103. ocount = ocount + 1
  104. observers[ocount] = func
  105. end
  106. function mobs.default_prototype:on_activate(staticdata)
  107. for i = 1, ocount
  108. do
  109. observers[i](self, staticdata)
  110. end
  111. self.object:set_armor_groups({fleshy = 100 - self.armor})
  112. if self.movement ~= "air" then
  113. self.object:setacceleration({x=0, y=mobs.gravity, z=0})
  114. end
  115. self.time_since_last_idle_sound = math.random() * self.idle_sound_cd
  116. self.id = math.random(0, 100000)
  117. end
  118. end
  119. do --safe remove
  120. local observers, ocount = {}, 0
  121. --for registering functions as callback
  122. mobs.register_on_default_remove = function(func)
  123. ocount = ocount + 1
  124. observers[ocount] = func
  125. end
  126. function mobs.default_prototype:remove()
  127. for i = 1, ocount
  128. do
  129. observers[i](self)
  130. end
  131. self.removed = true
  132. self.object:remove()
  133. end
  134. end
  135. --on_step
  136. do
  137. local observers, ocount = {}, 0
  138. --for registering functions as callback
  139. mobs.register_on_default_step = function(func)
  140. ocount = ocount + 1
  141. observers[ocount] = func
  142. end
  143. function mobs.default_prototype:on_step(dtime)
  144. local ceas = os.clock()
  145. for i = 1, ocount
  146. do
  147. observers[i](self, dtime)
  148. end
  149. self.cache_is_standing = nil
  150. self.cache_find_nearest_player = nil
  151. self.time_since_last_idle_sound = self.time_since_last_idle_sound + dtime
  152. if self.idle_sound_cd < self.time_since_last_idle_sound and
  153. math.random() < 0.3
  154. then
  155. self:make_sound("idle")
  156. self.time_since_last_idle_sound = 0
  157. end
  158. if self.pause_timer <= 0 then
  159. if self.destination then
  160. self:move(dtime, self.destination)
  161. if vector.distance(self.object:get_pos(), self.destination) < 0.5 then
  162. self.destination = nil
  163. end
  164. else
  165. self:move(dtime, self.object:get_pos())
  166. end
  167. else
  168. self.pause_timer = self.pause_timer - dtime
  169. end
  170. if self.movement ~= "air" and not self:is_standing() then
  171. self:set_animation("fall", {"jump", "attack", "move_attack"})
  172. end
  173. -- Remove when far enough and may not reach the player at all
  174. local nearest = self:find_target()
  175. if self.life_timer <= 0 then
  176. if nearest.distance > 12 then
  177. self:remove()
  178. end
  179. else
  180. self.life_timer = self.life_timer - dtime
  181. end
  182. -- Disable collision when far enough
  183. if self.collide_with_objects then
  184. if nearest.distance > 6 then
  185. self.collide_with_objects = false
  186. self.object:set_properties({collide_with_objects = self.collide_with_objects})
  187. end
  188. else
  189. if nearest.distance < 1.5 then
  190. self.collide_with_objects = true
  191. self.object:set_properties({collide_with_objects = self.collide_with_objects})
  192. end
  193. end
  194. self.timer = self.timer + dtime
  195. defense_mob_api:track_time("prototype on_step", os.clock() - ceas)
  196. end
  197. end
  198. --on_punch
  199. do
  200. local observers, ocount = {}, 0
  201. --for registering functions as callback
  202. mobs.register_on_default_punch = function(func)
  203. ocount = ocount + 1
  204. observers[ocount] = func
  205. end
  206. function mobs.default_prototype:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage)
  207. for i = 1, ocount
  208. do
  209. observers[i](self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
  210. end
  211. -- Weapon wear code adapted from TenPlus1's mobs redo (https://github.com/tenplus1/mobs)
  212. if puncher
  213. then
  214. local weapon = puncher:get_wielded_item()
  215. if tool_capabilities
  216. then
  217. local wear = (0.01) * (self.armor / 100) * 65534 + 1
  218. weapon:add_wear(wear)
  219. puncher:set_wielded_item(weapon)
  220. end
  221. end
  222. self:make_sound("hurt")
  223. dir.y = dir.y + 1
  224. local m = self.mass or 1
  225. local knockback = vector.multiply(vector.normalize(dir), 10 / (1 + m))
  226. self.object:setvelocity(vector.add(self.object:getvelocity(), knockback))
  227. self.pause_timer = 0.3
  228. if self.object:get_hp() - damage <= 0
  229. then
  230. self:die(puncher)
  231. end
  232. end
  233. end
  234. do --get_staticdata
  235. local observers, ocount = {}, 0
  236. --for registering functions as callback
  237. mobs.register_on_default_get_staticdata = function(func)
  238. ocount = ocount + 1
  239. observers[ocount] = func
  240. end
  241. function mobs.default_prototype:get_staticdata()
  242. local data = {}
  243. for i = 1, ocount
  244. do
  245. local key, val = observers[i](self)
  246. data[key] = val
  247. end
  248. return minetest.serialize(data)
  249. end
  250. end
  251. function mobs.default_prototype:damage(amount)
  252. local hp = self.object:get_hp()
  253. if hp <= amount then
  254. self:die()
  255. else
  256. self.object:set_hp(hp - amount)
  257. end
  258. end
  259. do --attack
  260. local observers, ocount = {}, 0
  261. --for registering functions as callback
  262. mobs.register_on_default_attack = function(func)
  263. ocount = ocount + 1
  264. observers[ocount] = func
  265. end
  266. function mobs.default_prototype:attack(obj, dir)
  267. for i = 1, ocount
  268. do
  269. observers[i](self, obj, dir)
  270. end
  271. self:make_sound("attack")
  272. obj:punch(self.object, self.timer - self.last_attack_time, {
  273. full_punch_interval=self.attack_interval,
  274. damage_groups = {fleshy=self.attack_damage}
  275. }, dir)
  276. end
  277. end
  278. function mobs.default_prototype:move(dtime, destination)
  279. mobs.move_method[self.movement](self, dtime, destination)
  280. end
  281. -- Attack the player
  282. function mobs.default_prototype:hunt()
  283. local nearest = self:find_target()
  284. if nearest.player then
  285. if nearest.distance <= self.attack_range then
  286. self:do_attack(nearest.player)
  287. end
  288. if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
  289. local pos = self.object:get_pos()
  290. local smart_dest = nil
  291. if self.smart_path then --Do pathfinding
  292. if self.smart_path_for > 0
  293. then
  294. self.smart_path_for = self.smart_path_for - 1
  295. end
  296. --add a little smarts if needed
  297. if self.smart_path_for >= 0 --this way it stays on if it's negative
  298. then
  299. local eyepos = {x = pos.x, y = pos.y + 0.5, z = pos.z}
  300. local los, obstacle = minetest.line_of_sight(nearest.position, eyepos)
  301. if obstacle and vector.distance(eyepos, obstacle) < 10
  302. then
  303. self.smart_path_for = self.smart_path_for + 2
  304. end
  305. end
  306. if smart_path_for ~= 0
  307. then
  308. smart_dest = defense_mob_api.pathfinder:get_waypoint(self.name, pos.x, pos.y, pos.z)
  309. end
  310. end
  311. if smart_dest then
  312. self.destination = smart_dest
  313. else
  314. local r = math.max(0, self.attack_range - 2)
  315. local dir = vector.aim(nearest.position, pos)
  316. self.destination = vector.add(nearest.position, vector.multiply(dir, r))
  317. end
  318. end
  319. end
  320. end
  321. function mobs.default_prototype:do_attack(obj)
  322. if self.last_attack_time + self.attack_interval < self.timer then
  323. local dir = vector.aim(self.object:get_pos(), obj:get_pos())
  324. self:attack(obj, dir)
  325. self.last_attack_time = self.timer
  326. if self.current_animation == "move" then
  327. self:set_animation("move_attack")
  328. else
  329. self:set_animation("attack")
  330. end
  331. end
  332. self.life_timer = math.min(300, self.life_timer + 60)
  333. end
  334. function mobs.default_prototype:jump(direction)
  335. if self:is_standing() then
  336. if direction then
  337. direction.y = 0.1
  338. direction = vector.normalize(direction)
  339. else
  340. direction = vec_zero()
  341. end
  342. local v = self.object:getvelocity()
  343. v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
  344. v.x = direction.x * self.jump_height
  345. v.z = direction.z * self.jump_height
  346. self.object:setvelocity(vector.add(self.object:getvelocity(), v))
  347. self:set_animation("jump")
  348. end
  349. end
  350. do --die
  351. local observers, ocount = {}, 0
  352. --for registering functions as callback
  353. mobs.register_on_default_die = function(func)
  354. ocount = ocount + 1
  355. observers[ocount] = func
  356. end
  357. function mobs.default_prototype:die(killer)
  358. for i = 1, ocount
  359. do
  360. observers[i](self, killer)
  361. end
  362. self.dead = true
  363. self:make_sound("die", true)
  364. self:on_death(killer)
  365. self:remove()
  366. end
  367. end
  368. function mobs.default_prototype:on_death(killer)
  369. end
  370. function mobs.default_prototype:is_standing()
  371. if self.cache_is_standing ~= nil then
  372. return self.cache_is_standing
  373. end
  374. if self.movement == "air" then
  375. self.cache_is_standing = false
  376. return false
  377. end
  378. if self.object:getvelocity().y ~= 0 then
  379. self.cache_is_standing = false
  380. return false
  381. end
  382. -- Check the four bottom corners for collision
  383. local p = self.object:get_pos()
  384. p.y = p.y + self.collisionbox[2] - 0.25
  385. local corners = {
  386. vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[3]}),
  387. vector.add(p, {x=self.collisionbox[1], y=0, z=self.collisionbox[6]}),
  388. vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[3]}),
  389. vector.add(p, {x=self.collisionbox[4], y=0, z=self.collisionbox[6]}),
  390. }
  391. for _,c in ipairs(corners) do
  392. local node = minetest.get_node_or_nil(c)
  393. if not node or
  394. (reg_nodes[node.name] or {walkable = true}).walkable
  395. then
  396. self.cache_is_standing = true
  397. return true
  398. end
  399. end
  400. self.cache_is_standing = false
  401. return false
  402. end
  403. function mobs.default_prototype:set_animation(name, inhibit)
  404. if self.current_animation == name then
  405. return
  406. end
  407. if inhibit then
  408. for _,p in ipairs(inhibit) do
  409. if self.current_animation == p and self.timer < self.current_animation_end then
  410. return
  411. end
  412. end
  413. end
  414. local anim_prop = self.animation[name]
  415. if anim_prop then
  416. self.current_animation = name
  417. self.current_animation_end = self.timer + (anim_prop.b - anim_prop.a - 1) / anim_prop.rate
  418. self.object:set_animation({x=anim_prop.a, y=anim_prop.b}, anim_prop.rate, 0)
  419. end
  420. end
  421. function mobs.default_prototype:find_target()
  422. if self.cache_find_nearest_player ~= nil then
  423. return self.cache_find_nearest_player
  424. end
  425. local p = self.object:get_pos()
  426. local nearest_player = nil
  427. local nearest_pos = p
  428. local nearest_dist = math.huge
  429. for _,obj in ipairs(get_relevant_players()) do --TODO change this
  430. local pos = obj:get_pos()
  431. pos.y = pos.y + 1
  432. local d = vector.distance(pos, p)
  433. if d < nearest_dist then
  434. nearest_player = obj
  435. nearest_pos = pos
  436. nearest_dist = d
  437. end
  438. end
  439. local ret = {player=nearest_player or self.object, position=nearest_pos, distance=nearest_dist}
  440. self.cache_find_nearest_player = ret
  441. return ret
  442. end
  443. -- Movement implementations for the default movement types
  444. mobs.move_method = {}
  445. function mobs.move_method:air(dtime, destination)
  446. local delta = vector.subtract(destination, self.object:get_pos())
  447. local dist = vector.length(delta)
  448. if dist ~= dist --nan check
  449. then
  450. return
  451. end
  452. -- Add random variation
  453. if dist > 3 then
  454. local r_angle = (self.id/100000) * 2 * math.pi
  455. local r_radius = (self.id/100000) * (dist - 3)/3
  456. delta = vector.add(delta, {
  457. x=math.cos(r_angle)*r_radius,
  458. y=r_radius,
  459. z=math.sin(r_angle)*r_radius
  460. })
  461. end
  462. -- Compute speed and smoothing factor
  463. local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
  464. local t
  465. local v = self.object:getvelocity()
  466. if vector.length(v) < self.move_speed * 1.5 then
  467. t = math.pow(0.1, dtime)
  468. else
  469. t = math.pow(0.4, dtime)
  470. speed = speed * 0.9
  471. end
  472. --[[Note:
  473. in lua
  474. x = a and b or c
  475. means
  476. if a then x = b else x = c
  477. ]]
  478. -- Compute and set resulting velocity
  479. self.object:setvelocity(vector.add(
  480. vector.multiply(v, t),
  481. vector.multiply(dist > 0 and vector.normalize(delta) or vec_zero(), speed * (1-t))
  482. ))
  483. if speed > self.move_speed * 0.04 then
  484. self:set_animation("move", {"attack", "move_attack"})
  485. else
  486. self:set_animation("idle", {"attack", "move_attack"})
  487. end
  488. end
  489. function mobs.move_method:ground(dtime, destination)
  490. local delta = vector.subtract(destination, self.object:get_pos())
  491. delta.y = 0
  492. local dist = vector.length(delta)
  493. if dist ~= dist --nan check
  494. then
  495. return
  496. end
  497. -- Add random variation
  498. if dist > 4 then
  499. local r_angle = (self.id/100000) * 2 * math.pi
  500. local r_radius = (dist - 4)/4
  501. delta = vector.add(delta, {
  502. x=math.cos(r_angle)*r_radius,
  503. y=0,
  504. z=math.sin(r_angle)*r_radius
  505. })
  506. end
  507. -- Compute speed and smoothing factor
  508. local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
  509. local t
  510. local v = self.object:getvelocity()
  511. if self:is_standing() and vector.length(v) < self.move_speed * 4 then
  512. t = math.pow(0.001, dtime)
  513. else
  514. t = math.pow(0.4, dtime)
  515. speed = speed * 0.9
  516. end
  517. -- Compute and set resulting velocity
  518. local dir = dist > 0 and vector.normalize(delta) or vec_zero()
  519. local v2 = vector.add(
  520. vector.multiply(v, t),
  521. vector.multiply(dir, speed * (1-t))
  522. )
  523. v2.y = v.y
  524. self.object:setvelocity(v2)
  525. local jump = nil
  526. local pos = self.object:get_pos()
  527. -- Check for jump
  528. if dist > 1 then
  529. --Jump over obstacles
  530. local p = vector.new(pos)
  531. p.y = p.y + self.collisionbox[2] + 0.5
  532. local sx = self.collisionbox[4] - self.collisionbox[1]
  533. local sz = self.collisionbox[6] - self.collisionbox[3]
  534. local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
  535. local fronts = {} --might be worth caching those
  536. do
  537. local xedge, zedge
  538. local offset = 0.2
  539. if math.sign(delta.x) < 0
  540. then
  541. xedge = self.collisionbox[1] - offset
  542. else
  543. xedge = self.collisionbox[4] + offset
  544. end
  545. if math.sign(delta.z) < 0
  546. then
  547. zedge = self.collisionbox[3] - offset
  548. else
  549. zedge = self.collisionbox[6] + offset
  550. end
  551. local index = 1
  552. --point to each node on the sides
  553. for i = 0, sz
  554. do
  555. fronts[index] = {x = self.collisionbox[1] + i, y = 0, z = zedge}
  556. index = index + 1
  557. end
  558. for i = 0, sx
  559. do
  560. fronts[index] = {x = xedge, y = 0, z = self.collisionbox[3] + i}
  561. index = index + 1
  562. end
  563. --also check the corner
  564. fronts[index] = {x = xedge, y = 0, z = zedge}
  565. end
  566. for _,f in ipairs(fronts) do
  567. local node = minetest.get_node_or_nil(vector.add(p, f))
  568. -- or {walkable = true} snippet to avoid indexing nil values with unknown nodes
  569. if not node or
  570. (reg_nodes[node.name] or {walkable = true}).walkable
  571. then
  572. jump = vector.aim(pos, destination)
  573. break
  574. end
  575. end
  576. elseif destination.y > pos.y + 1
  577. then
  578. jump = vector.aim(pos, destination)
  579. end
  580. if jump then
  581. self:jump(jump)
  582. elseif self:is_standing() then
  583. if speed > self.move_speed * 0.06 then
  584. self:set_animation("move", {"move_attack"})
  585. else
  586. self:set_animation("idle", {"attack", "move_attack"})
  587. end
  588. end
  589. end
  590. function mobs.register_mob(name, def)
  591. local prototype = {}
  592. --copy default prototype
  593. for k,v in pairs(mobs.default_prototype) do
  594. prototype[k] = v
  595. end
  596. --overwrite fields of default prototype that def uses
  597. for k,v in pairs(def) do
  598. prototype[k] = v
  599. end
  600. prototype.media_prefix = (string.gsub(name, ":", "_") .. "_")
  601. prototype.move = def.move or mobs.move_method[prototype.movement]
  602. -- Register for pathfinding
  603. if defense_mob_api.pathfinder and prototype.smart_path then
  604. defense_mob_api.pathfinder:register_class(name, {
  605. collisionbox = prototype.collisionbox,
  606. jump_height = math.floor(prototype.jump_height),
  607. path_check = def.pathfinder_check or defense_mob_api.pathfinder.default_path_check[prototype.movement],
  608. cost_method = def.pathfinder_cost or defense_mob_api.pathfinder.default_cost_method[prototype.movement],
  609. })
  610. end
  611. --Register for swarming
  612. if prototype.swarm
  613. then
  614. prototype.swarm = defense_mob_api.Swarm()
  615. end
  616. minetest.register_entity(name, prototype)
  617. end