init.lua 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. pm = pm or {}
  2. pm.modpath = minetest.get_modpath("pm")
  3. pm.sight_range = 30
  4. -- Pathfinder cooldown min/max time.
  5. pm.pf_cooldown_min = 5
  6. pm.pf_cooldown_max = 15
  7. -- Cooldown timer to acquire a new target.
  8. pm.aq_cooldown_min = 1
  9. pm.aq_cooldown_max = 10
  10. -- Range at which entity is considered to have found its target.
  11. pm.range = 2
  12. pm.velocity = 3
  13. dofile(pm.modpath .. "/seek.lua")
  14. dofile(pm.modpath .. "/action.lua")
  15. function pm.target_is_player_or_mob(target)
  16. if target:is_player() then
  17. return true
  18. end
  19. local ent = target:get_luaentity()
  20. if ent.mob then
  21. return true
  22. end
  23. end
  24. function pm.debug_chat(text)
  25. -- Comment or uncomment as needed for debugging.
  26. --minetest.chat_send_all(text)
  27. end
  28. function pm.debug_path(path)
  29. -- Ditto.
  30. --for k, v in ipairs(path) do pm.spawn_path_particle(v) end
  31. end
  32. function pm.debug_goal(pos)
  33. -- Ditto.
  34. --pm.spawn_path_particle(pos)
  35. --pm.death_particle_effect(pos)
  36. end
  37. function pm.death_particle_effect(pos)
  38. local particles = {
  39. amount = 100,
  40. time = 1.1,
  41. minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
  42. maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
  43. minvel = vector.new(-3.5, -3.5, -3.5),
  44. maxvel = vector.new(3.5, 3.5, 3.5),
  45. minacc = {x=0, y=0, z=0},
  46. maxacc = {x=0, y=0, z=0},
  47. minexptime = 1.5,
  48. maxexptime = 2.0,
  49. minsize = 0.5,
  50. maxsize = 1.0,
  51. collisiondetection = false,
  52. collision_removal = false,
  53. vertical = false,
  54. texture = "quartz_crystal_piece.png",
  55. glow = 14,
  56. --attached = entity,
  57. }
  58. minetest.add_particlespawner(particles)
  59. end
  60. -- Get objects inside radius, but remove self from the returned list.
  61. function pm.get_nearby_objects(self, pos, radius)
  62. local objects = minetest.get_objects_inside_radius(pos, radius)
  63. if self._identity then
  64. for i=1, #objects, 1 do
  65. local ent = objects[i]:get_luaentity()
  66. if ent and ent._identity then
  67. if ent._identity == self._identity then
  68. table.remove(objects, i)
  69. break
  70. end
  71. end
  72. end
  73. end
  74. return objects
  75. end
  76. function pm.spawn_path_particle(pos)
  77. local particles = {
  78. amount = 1,
  79. time = 0.1,
  80. minpos = pos,
  81. maxpos = pos,
  82. minvel = {x=0, y=0, z=0},
  83. maxvel = {x=0, y=0, z=0},
  84. minacc = {x=0, y=0, z=0},
  85. maxacc = {x=0, y=0, z=0},
  86. minexptime = 5,
  87. maxexptime = 5,
  88. minsize = 2.0,
  89. maxsize = 2.0,
  90. collisiondetection = false,
  91. collision_removal = false,
  92. vertical = false,
  93. texture = "default_mese_crystal.png",
  94. glow = 14,
  95. --attached = entity,
  96. }
  97. minetest.add_particlespawner(particles)
  98. end
  99. function pm.follower_spawn_particles(pos, entity)
  100. local particles = {
  101. amount = 10,
  102. time = 1,
  103. minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
  104. maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
  105. minvel = vector.new(-0.5, -0.5, -0.5),
  106. maxvel = vector.new(0.5, 0.5, 0.5),
  107. minacc = {x=0, y=0, z=0},
  108. maxacc = {x=0, y=0, z=0},
  109. minexptime = 0.5,
  110. maxexptime = 2.0,
  111. minsize = 0.5,
  112. maxsize = 1.0,
  113. collisiondetection = false,
  114. collision_removal = false,
  115. vertical = false,
  116. texture = "quartz_crystal_piece.png",
  117. glow = 14,
  118. --attached = entity,
  119. }
  120. minetest.add_particlespawner(particles)
  121. end
  122. function pm.follower_on_activate(self, staticdata, dtime_s)
  123. if staticdata and staticdata ~= "" then
  124. local data = minetest.deserialize(staticdata)
  125. if type(data) == "table" then
  126. for k, v in pairs(data) do
  127. pm.debug_chat("on_activate(): self["..k.."]="..tostring(v))
  128. self[k] = v
  129. end
  130. return
  131. end
  132. end
  133. -- Otherwise, set up default data.
  134. self._timer = self._timer or 0
  135. self._lifetime = self._lifetime or 60*60*24
  136. self._sound_time = self._sound_time or 0
  137. end
  138. function pm.follower_get_staticdata(self)
  139. local data = {}
  140. for k, v in pairs(self) do
  141. local t = type(v)
  142. if t == "number" or t == "string" or t == "boolean" then
  143. if k:find("_") == 1 then
  144. pm.debug_chat("get_staticdata(): data["..k.."]="..tostring(v))
  145. data[k] = v
  146. end
  147. end
  148. end
  149. return minetest.serialize(data) or ""
  150. end
  151. function pm.follower_on_step(self, dtime, moveresult)
  152. -- Remove object once we're old enough.
  153. if not self._lifetime then
  154. self.object:remove()
  155. return
  156. end
  157. self._lifetime = self._lifetime - dtime
  158. if self._lifetime < 0 then
  159. self.object:remove()
  160. return
  161. end
  162. -- Cooldown timer for the pathfinder, since using it is intensive.
  163. if self._failed_pathfind_cooldown then
  164. self._failed_pathfind_cooldown = self._failed_pathfind_cooldown - dtime
  165. if self._failed_pathfind_cooldown < 0 then
  166. self._failed_pathfind_cooldown = nil
  167. -- Also reset path/target info so we have a chance to acquire fresh data.
  168. self._goto = nil
  169. self._path = nil
  170. end
  171. end
  172. if self._acquire_target_cooldown then
  173. self._acquire_target_cooldown = self._acquire_target_cooldown - dtime
  174. if self._acquire_target_cooldown < 0 then
  175. self._acquire_target_cooldown = nil
  176. end
  177. end
  178. if self._wander_cooldown then
  179. self._wander_cooldown = self._wander_cooldown - dtime
  180. if self._wander_cooldown < 0 then
  181. self._wander_cooldown = nil
  182. end
  183. end
  184. -- Entity changes its behavior every so often.
  185. if not self._behavior_timer or self._behavior_timer < 0 then
  186. self._behavior_timer = math.random(1, 20)*60
  187. pm.choose_random_behavior(self)
  188. end
  189. self._behavior_timer = self._behavior_timer - dtime
  190. -- Entities sometimes get stuck against objects.
  191. -- Unstick them by rounding their positions to the nearest air node.
  192. if not self._unstick_timer or self._unstick_timer < 0 then
  193. self._unstick_timer = math.random(1, 30)
  194. local air = minetest.find_node_near(self.object:get_pos(), 1, "air", true)
  195. if air then
  196. self.object:set_pos(air)
  197. end
  198. end
  199. self._unstick_timer = self._unstick_timer - dtime
  200. -- Sound timer.
  201. if not self._sound_time then self._sound_time = 0 end
  202. self._sound_time = self._sound_time - dtime
  203. if self._sound_time < 0 then
  204. self._sound_time = math.random(100, 300)/100
  205. ambiance.sound_play("wisp", self.object:get_pos(), 0.2, 32)
  206. end
  207. -- If currently following a path, remove waypoints as we reach them.
  208. if self._path and #(self._path) > 0 then
  209. local p = self._path[1]
  210. -- Remove waypoint from path if we've reached it.
  211. if vector.distance(p, self.object:get_pos()) < 0.5 then
  212. pm.debug_chat('hit waypoint')
  213. self._stuck_timer = 2
  214. table.remove(self._path, 1)
  215. end
  216. -- Remove path when all waypoints exhausted.
  217. if #(self._path) < 1 then
  218. pm.debug_chat('finished following path: ' .. minetest.pos_to_string(self._goto))
  219. pm.debug_chat('node at terminus: ' .. minetest.get_node(self._goto).name)
  220. self._path = nil
  221. self._path_is_los = nil
  222. self._stuck_timer = nil
  223. self._failed_pathfind_cooldown = nil
  224. self.object:set_velocity({x=0, y=0, z=0})
  225. end
  226. if self._stuck_timer then
  227. self._stuck_timer = self._stuck_timer - dtime
  228. if self._stuck_timer < 0 then
  229. pm.debug_chat('got stuck following path')
  230. -- Got stuck trying to follow path.
  231. -- This is caused because the entity is physical and may collide with
  232. -- the environment. Blocks may have been added in the entity's path, or
  233. -- (more usually) the entity did not properly navigate a corner.
  234. -- We should seek a new target.
  235. self._goto = nil
  236. self._path = nil
  237. self._target = nil
  238. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  239. self._stuck_timer = nil
  240. self._wander_cooldown = nil
  241. self.object:set_velocity({x=0, y=0, z=0})
  242. end
  243. end
  244. end
  245. -- Main logic timer.
  246. -- This controls how often the main "AI" logic runs.
  247. self._timer = self._timer + dtime
  248. if self._timer < 0.3 then return end
  249. self._timer = 0
  250. -- Spawn particles to indicate our location.
  251. local pos = self.object:get_pos()
  252. pm.follower_spawn_particles(pos, self.object)
  253. -- Find new target/goal-waypoint if we don't have one.
  254. if not self._acquire_target_cooldown and not self._failed_pathfind_cooldown then
  255. if not self._goto and not self._path then
  256. local tp, target = self._interest_point(self, pos)
  257. if target then
  258. -- Target is another entity.
  259. pm.debug_chat('acquired moving target')
  260. --if target:is_player() then pm.debug_chat('targeting player') end
  261. local s = tp
  262. if not s then s = target:get_pos() end
  263. if s then
  264. s = vector.round(s)
  265. if pm.target_is_player_or_mob(target) then
  266. s.y = s.y + 1 -- For players or mobs, seek above them, not at their feet.
  267. end
  268. s = minetest.find_node_near(s, 1, "air", true)
  269. -- Target must be standing in air.
  270. -- Otherwise it would never be reachable.
  271. if s then
  272. -- Don't reacquire target if we're already sitting on it.
  273. if vector.distance(pos, s) > pm.range then
  274. pm.debug_chat('set moving target goal')
  275. self._goto = vector.round(s)
  276. self._target = target -- Userdata object.
  277. end
  278. end
  279. end
  280. self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
  281. elseif tp then
  282. -- Target is a static location.
  283. pm.debug_chat('acquired static target')
  284. -- Don't reacquire target if we're already sitting on it.
  285. if vector.distance(pos, tp) > pm.range then
  286. pm.debug_chat('set static target goal')
  287. self._goto = vector.round(tp)
  288. self._target = nil
  289. end
  290. self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
  291. else
  292. -- No target acquired. Wait awhile before calling function again.
  293. pm.debug_chat('no target acquired')
  294. self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
  295. end
  296. end
  297. end
  298. -- Get a path to our target if we don't have a path yet, and target is not nearby.
  299. if not self._failed_pathfind_cooldown then
  300. if self._goto and not self._path and vector.distance(self._goto, pos) > pm.range then
  301. pm.debug_chat('want path to target')
  302. local los, obstruction = minetest.line_of_sight(vector.round(pos), vector.round(self._goto))
  303. if los then
  304. -- We have LOS (line of sight) direct to target.
  305. pm.debug_chat('LOS confirmed')
  306. local dir = vector.subtract(vector.round(self._goto), vector.round(pos))
  307. local dst = vector.length(dir)
  308. dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
  309. -- Assemble a straight-line path.
  310. local path = {}
  311. for i=1, dst, 1 do
  312. path[#path+1] = vector.add(pos, vector.multiply(dir, i))
  313. end
  314. if #path > 0 then
  315. self._path = path
  316. self._path_is_los = true
  317. self._stuck_timer = nil
  318. end
  319. else
  320. -- No line of sight to target. Use pathfinder!
  321. pm.debug_chat('will try pathfinder')
  322. local rp1 = vector.round(pos)
  323. local rp2 = vector.round(self._goto)
  324. local a1 = rp1
  325. local a2 = rp2
  326. local d1 = minetest.registered_nodes[minetest.get_node(rp1).name]
  327. local d2 = minetest.registered_nodes[minetest.get_node(rp2).name]
  328. -- If either start or end are non-walkable, we don't need to look for air.
  329. if d1.walkable then
  330. a1 = minetest.find_node_near(rp1, 2, "air", true)
  331. end
  332. if d2.walkable then
  333. a2 = minetest.find_node_near(rp2, 2, "air", true)
  334. end
  335. if a1 and a2 then
  336. pm.debug_chat('start and end position are both in air')
  337. local prepath = {table.copy(a1)}
  338. local postpath = {table.copy(a2)}
  339. -- Find air directly above ground for the wisp's start position.
  340. -- This is necessary because the wisp usually flies a little bit above
  341. -- the ground. Pathfinding will fail if we don't start at ground level.
  342. while minetest.get_node(vector.add(a1, {x=0, y=-1, z=0})).name == "air" do
  343. a1.y = a1.y - 1
  344. table.insert(prepath, table.copy(a1))
  345. end
  346. -- Find air directly above ground for the target position.
  347. local target_y = a2.y
  348. while minetest.get_node(vector.add(a2, {x=0, y=-1, z=0})).name == "air" do
  349. a2.y = a2.y - 1
  350. table.insert(postpath, 1, table.copy(a2))
  351. end
  352. -- If this triggers then the target is flying, or hanging over a high ledge.
  353. if (target_y - a2.y) > 2 then
  354. end
  355. -- The shorter the apparent distance between these 2 points, the farther
  356. -- we can afford to look around.
  357. local d = vector.distance(a1, a2)
  358. local r = math.max(1, math.floor(pm.sight_range - d))
  359. pm.debug_chat("trying to find path")
  360. self._path = minetest.find_path(a1, a2, r, 1, 1, "A*_noprefetch")
  361. if not self._path then
  362. pm.debug_chat('no path found')
  363. -- If we couldn't find a path to this location, we should remove this
  364. -- goal. Also set the pathfinder cooldown timer.
  365. self._goto = nil
  366. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  367. else
  368. if #(self._path) >= 1 then
  369. pm.debug_chat("got path")
  370. self._stuck_timer = nil
  371. pm.debug_chat('welding pre and post paths')
  372. local path = {}
  373. for i=1, #prepath, 1 do
  374. path[#path+1] = prepath[i]
  375. end
  376. for i=1, #(self._path), 1 do
  377. path[#path+1] = self._path[i]
  378. end
  379. for i=1, #postpath, 1 do
  380. path[#path+1] = postpath[i]
  381. end
  382. self._path = path
  383. self._stuck_timer = nil
  384. -- Debug render path.
  385. pm.debug_path(self._path)
  386. -- If start and end points are equal, toss this path out.
  387. -- Also set the pathfinder cooldown timer.
  388. if vector.equals(self._path[1], self._path[#(self._path)]) then
  389. pm.debug_chat('tossing path because start and end are equal')
  390. self._path = nil
  391. self._goto = nil
  392. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  393. end
  394. -- If path's start position is too far away, we can't use the path.
  395. if self._path then
  396. if vector.distance(self._path[1], pos) > pm.range then
  397. pm.debug_chat('tossing path because start is too far away')
  398. self._path = nil
  399. self._goto = nil
  400. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  401. end
  402. end
  403. else
  404. -- Not a real path!
  405. -- Must have at least one position.
  406. pm.debug_chat('tossing path because it is bogus')
  407. self._goto = nil
  408. self._path = nil
  409. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  410. end
  411. end
  412. else
  413. -- One or both positions not accessible (no nearby air).
  414. -- Thus we must give up this target.
  415. self._goto = nil
  416. self._path = nil
  417. self._failed_pathfind_cooldown = math.random(pm.pf_cooldown_min, pm.pf_cooldown_max)
  418. end
  419. end
  420. end
  421. end
  422. -- Follow current path.
  423. if self._path and #(self._path) > 0 then
  424. -- For paths of longer than trivial length, try to optimize with LOS.
  425. -- We can do this because this mob can fly over gaps and such. This also
  426. -- makes the movement look better.
  427. if #(self._path) > 5 then
  428. -- Don't do LOS optimization if the current waypoint is already so marked.
  429. local p = self._path[2]
  430. if not p.los then
  431. while #(self._path) > 1 and minetest.line_of_sight(pos, p) do
  432. table.remove(self._path, 1)
  433. p = self._path[2]
  434. end
  435. local dir = vector.subtract(self._path[1], pos)
  436. local dst = vector.length(dir)
  437. dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
  438. -- Assemble a straight-line path.
  439. local path = {}
  440. for i=1, dst, 1 do
  441. path[#path+1] = vector.add(pos, vector.multiply(dir, i))
  442. path[#path].los = true -- Mark waypoint as a LOS point.
  443. end
  444. -- Append the remainder of the real path.
  445. for i=1, #(self._path), 1 do
  446. path[#path+1] = self._path[i]
  447. end
  448. -- Set new path.
  449. self._path = path
  450. self._stuck_timer = nil
  451. -- Debug render path.
  452. pm.debug_path(self._path)
  453. end
  454. end
  455. pm.debug_chat('following path')
  456. local waypoint = self._path[1]
  457. local waynode = minetest.get_node(waypoint)
  458. -- Check if path runs through an obstruction.
  459. -- Nodes must be 'air' or non-walkable (like plants).
  460. local obstructed = false
  461. if waynode.name ~= "air" then
  462. local ndef = minetest.registered_nodes[waynode.name]
  463. if not ndef or ndef.walkable then
  464. obstructed = true
  465. end
  466. end
  467. if not obstructed then
  468. if self._path_is_los or waypoint.los then
  469. -- Follow line-of-sight paths directly.
  470. --self.object:move_to(waypoint, true)
  471. --table.remove(self._path, 1)
  472. -- Smooth movement.
  473. local dir = vector.subtract(waypoint, pos)
  474. if vector.length(dir) > 0.4 then
  475. dir = vector.normalize(dir)
  476. dir = vector.multiply(dir, pm.velocity)
  477. self.object:set_velocity(dir)
  478. end
  479. else
  480. -- Cause entity to float 1.5 meters above ground when following path,
  481. -- if there's enough head room. But not for the last waypoint in the path.
  482. waypoint.y = waypoint.y + 1
  483. local n = minetest.get_node(waypoint)
  484. if n.name == "air" and #(self._path) > 1 then
  485. waypoint.y = waypoint.y - 0.5
  486. --self.object:move_to(waypoint, true)
  487. --table.remove(self._path, 1)
  488. -- Smooth movement.
  489. local dir = vector.subtract(waypoint, pos)
  490. if vector.length(dir) > 0.4 then
  491. dir = vector.normalize(dir)
  492. dir = vector.multiply(dir, pm.velocity)
  493. self.object:set_velocity(dir)
  494. end
  495. else
  496. waypoint.y = waypoint.y - 1
  497. --self.object:move_to(waypoint, true)
  498. --table.remove(self._path, 1)
  499. -- Smooth movement.
  500. local dir = vector.subtract(waypoint, pos)
  501. if vector.length(dir) > 0.4 then
  502. dir = vector.normalize(dir)
  503. dir = vector.multiply(dir, pm.velocity)
  504. self.object:set_velocity(dir)
  505. end
  506. end
  507. end
  508. else
  509. -- Path obstructed. Need new path, this one is bad.
  510. pm.debug_chat('path obstructed: ' .. waynode.name)
  511. self._path = nil
  512. self._path_is_los = nil
  513. self._goto = nil
  514. self.object:set_velocity({x=0, y=0, z=0})
  515. end
  516. end
  517. -- Dynamic targets can move while we're trying to path to them.
  518. -- Update path as long as we have LOS to the target.
  519. if self._target and self._path and self._goto then
  520. local target_pos = self._target:get_pos()
  521. if target_pos then
  522. if #(self._path) > 0 then
  523. local end_path = self._path[#(self._path)]
  524. if vector.distance(target_pos, end_path) > 3 then
  525. local los, obstruction = minetest.line_of_sight(vector.round(pos), vector.round(target_pos))
  526. if los then
  527. pm.debug_chat('target moved, repathing via LOS')
  528. self._goto = vector.round(target_pos)
  529. local dir = vector.subtract(self._goto, vector.round(pos))
  530. local dst = vector.length(dir)
  531. dir = vector.normalize(dir) -- Returns 0,0,0 for zero-length vector.
  532. -- Assemble a straight-line path.
  533. local path = {}
  534. for i=1, dst, 1 do
  535. path[#path+1] = vector.add(pos, vector.multiply(dir, i))
  536. end
  537. if #path > 0 then
  538. self._path = path
  539. self._path_is_los = true
  540. self._stuck_timer = nil
  541. end
  542. end
  543. end
  544. end
  545. end
  546. end
  547. -- Remove target waypoint once we're close enough to it.
  548. -- Only if done following path.
  549. if self._goto and not self._path then
  550. pm.debug_chat('distance to goal: ' .. vector.distance(self._goto, pos))
  551. pm.debug_goal(self._goto)
  552. if vector.distance(self._goto, pos) < pm.range then
  553. pm.debug_chat('reached goal')
  554. --self.object:move_to(self._goto, true)
  555. -- Have we arrived at the target (if we did indeed have a target)?
  556. if self._target then
  557. local s = self._target:get_pos()
  558. if s then
  559. s = vector.round(s)
  560. s.y = s.y + 1 -- For entities, we seek above them, not at their feet.
  561. if vector.distance(pos, s) < pm.range then
  562. pm.debug_chat('reached dynamic target')
  563. -- We have reached our moveable target.
  564. -- We can clear this and set a timer to delay acquiring the next target.
  565. self._on_arrival(self, self._goto, self._target)
  566. self._goto = nil
  567. self._target = nil
  568. self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
  569. else
  570. -- Our moveable target has moved. We must move toward it again.
  571. -- Do so right away, without delay.
  572. pm.debug_chat('target has moved, reacquiring')
  573. self._goto = s
  574. self._acquire_target_cooldown = nil
  575. self._failed_pathfind_cooldown = nil
  576. end
  577. else
  578. -- Moving target no longer available. We must clear this.
  579. self._target = nil
  580. self._goto = nil
  581. end
  582. else
  583. pm.debug_chat('reached static target')
  584. self._on_arrival(self, self._goto, nil)
  585. -- No moving target, so we can clear this.
  586. self._goto = nil
  587. self._acquire_target_cooldown = math.random(pm.aq_cooldown_min, pm.aq_cooldown_max)
  588. end
  589. end
  590. end
  591. -- Drift behavior, as long as we don't have a target to go to.
  592. if not self._wander_cooldown then
  593. if not self._goto then
  594. local dir = {
  595. x = math.random(-1, 1)/10,
  596. y = math.random(-1, 1)/10,
  597. z = math.random(-1, 1)/10
  598. }
  599. self.object:set_velocity(dir)
  600. self._wander_cooldown = math.random(1, 5)
  601. end
  602. end
  603. end
  604. function pm.follower_on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir)
  605. local pos = self.object:get_pos()
  606. pm.death_particle_effect(pos)
  607. minetest.add_item(pos, "glowstone:glowing_dust " .. math.random(1, 3))
  608. self.object:remove()
  609. end
  610. -- Create entity at position, if possible.
  611. function pm.spawn_wisp(pos, behavior)
  612. pos = vector.round(pos)
  613. local node = minetest.get_node(pos)
  614. if node.name == "air" then
  615. local ent = minetest.add_entity(pos, "pm:follower")
  616. if ent then
  617. local luaent = ent:get_luaentity()
  618. if luaent then
  619. luaent._behavior = behavior
  620. luaent._behavior_timer = math.random(1, 20)*60
  621. -- Allows to uniquely identify the wisp to other wisps, with little chance of collision.
  622. -- In particular this allows the wisp to ignore itself in any object queries.
  623. luaent._identity = math.random(1, 32000)
  624. -- This is so the wisp knows where it spawned at.
  625. -- We format it as a string so that this data is saved statically.
  626. luaent._spawn_origin = minetest.pos_to_string(pos)
  627. return ent
  628. else
  629. ent:remove()
  630. end
  631. end
  632. end
  633. end
  634. local behaviors = {
  635. "follower",
  636. "pest",
  637. "thief",
  638. "healer",
  639. "explorer",
  640. "boom",
  641. "communal",
  642. "solitary",
  643. "guard",
  644. "arsonist",
  645. "porter",
  646. }
  647. function pm.choose_random_behavior(self)
  648. self._behavior = behaviors[math.random(1, #behaviors)]
  649. -- Don't chose a self-destructive behavior by chance.
  650. if self._behavior == "boom" then
  651. self._behavior = "follower"
  652. end
  653. end
  654. -- Create entity at position, if possible.
  655. function pm.spawn_random_wisp(pos)
  656. local act = behaviors[math.random(1, #behaviors)]
  657. if act == "boom" then
  658. act = "follower"
  659. end
  660. return pm.spawn_wisp(pos, act)
  661. end
  662. -- Table of functions for obtaining interest points.
  663. local interests = {
  664. follower = function(self, pos)
  665. return pm.seek_player_or_mob_or_item(self, pos)
  666. end,
  667. thief = function(self, pos)
  668. return pm.seek_player_or_item(self, pos)
  669. end,
  670. pest = function(self, pos)
  671. return pm.seek_player(self, pos)
  672. end,
  673. healer = function(self, pos)
  674. return pm.seek_player(self, pos)
  675. end,
  676. explorer = function(self, pos)
  677. return pm.seek_node_with_meta(self, pos)
  678. end,
  679. -- Suicide, never chosen at random.
  680. boom = function(self, pos)
  681. return pm.seek_player_or_mob(self, pos)
  682. end,
  683. communal = function(self, pos)
  684. return pm.seek_wisp(self, pos)
  685. end,
  686. solitary = function(self, pos)
  687. return pm.seek_solitude(self, pos)
  688. end,
  689. guard = function(self, pos)
  690. -- Seek target in sight-range of spawn origin, otherwise return to origin.
  691. if self._spawn_origin then
  692. local origin = minetest.string_to_pos(self._spawn_origin)
  693. if origin then
  694. if vector.distance(origin, self.object:get_pos()) > pm.sight_range then
  695. return origin, nil
  696. else
  697. -- Within sight range of spawn origin, seek target.
  698. return pm.seek_player_or_mob_not_wisp(self, pos)
  699. end
  700. end
  701. end
  702. return nil, nil
  703. end,
  704. arsonist = function(self, pos)
  705. local target = pm.seek_flammable_node(self, pos)
  706. if not target then
  707. return pm.seek_player_or_mob(self, pos)
  708. end
  709. return target, nil
  710. end,
  711. porter = function(self, pos)
  712. return pm.seek_player(self, pos)
  713. end,
  714. }
  715. -- Table of possible action functions to take on arriving at a target.
  716. local actions = {
  717. pest = function(self, pos, target)
  718. pm.hurt_nearby_players(self)
  719. end,
  720. healer = function(self, pos, target)
  721. pm.heal_nearby_players(self)
  722. end,
  723. thief = function(self, pos, target)
  724. pm.steal_nearby_item(self, target)
  725. end,
  726. boom = function(self, pos, target)
  727. pm.explode_nearby_target(self, target)
  728. end,
  729. guard = function(self, pos, target)
  730. -- Attack target, but only if in sight-range of spawn origin.
  731. if self._spawn_origin then
  732. local origin = minetest.string_to_pos(self._spawn_origin)
  733. if origin then
  734. if vector.distance(origin, self.object:get_pos()) < pm.sight_range then
  735. pm.hurt_nearby_player_or_mob_not_wisp(self)
  736. end
  737. end
  738. end
  739. end,
  740. arsonist = function(self, pos, target)
  741. pm.commit_arson_at_target(pos)
  742. end,
  743. porter = function(self, pos, target)
  744. pm.teleport_player_to_prior_location(target)
  745. end,
  746. }
  747. function pm.interest_point(self, pos)
  748. if self._behavior then
  749. if interests[self._behavior] then
  750. return interests[self._behavior](self, pos)
  751. end
  752. end
  753. return nil, nil
  754. end
  755. function pm.on_arrival(self, pos, other)
  756. pm.debug_chat('arrived at target')
  757. if self._behavior then
  758. pm.debug_chat('have behavior: ' .. self._behavior)
  759. if actions[self._behavior] then
  760. actions[self._behavior](self, pos, other)
  761. end
  762. end
  763. end
  764. if not pm.registered then
  765. local entity = {
  766. initial_properties = {
  767. visual = "cube",
  768. textures = {
  769. "quartz_crystal_piece.png",
  770. "quartz_crystal_piece.png",
  771. "quartz_crystal_piece.png",
  772. "quartz_crystal_piece.png",
  773. "quartz_crystal_piece.png",
  774. "quartz_crystal_piece.png",
  775. },
  776. visual_size = {x=0.2, y=0.2, z=0.2},
  777. collide_with_objects = false,
  778. pointable = false,
  779. is_visible = true,
  780. makes_footstep_sound = false,
  781. glow = 14,
  782. automatic_rotate = 0.5,
  783. -- This is so that wandering/drifting wisps don't bury themselves.
  784. physical = true,
  785. collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2},
  786. },
  787. -- So other game code can tell what this entity is.
  788. _name = "pm:follower",
  789. description = "Seon",
  790. mob = true,
  791. on_step = function(...) return pm.follower_on_step(...) end,
  792. on_punch = function(...) return pm.follower_on_punch(...) end,
  793. on_activate = function(...) return pm.follower_on_activate(...) end,
  794. get_staticdata = function(...) return pm.follower_get_staticdata(...) end,
  795. _interest_point = function(...) return pm.interest_point(...) end,
  796. _on_arrival = function(...) return pm.on_arrival(...) end,
  797. }
  798. minetest.register_entity("pm:follower", entity)
  799. local c = "pm:core"
  800. local f = pm.modpath .. "/init.lua"
  801. reload.register_file(c, f, false)
  802. pm.registered = true
  803. end