init.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. pm = pm or {}
  2. pm.modpath = minetest.get_modpath("pm")
  3. pm.sight_range = 30
  4. pm.nest_range = 15
  5. -- Pathfinder cooldown min/max time.
  6. pm.pf_cooldown_min = 5
  7. pm.pf_cooldown_max = 15
  8. -- Cooldown timer to acquire a new target.
  9. pm.aq_cooldown_min = 1
  10. pm.aq_cooldown_max = 10
  11. -- Range at which entity is considered to have found its target.
  12. pm.range = 2
  13. pm.velocity = 3
  14. pm.run_velocity = 4.5
  15. pm.walk_velocity = 2
  16. -- Localize for performance.
  17. local vector_distance = vector.distance
  18. local vector_round = vector.round
  19. local math_floor = math.floor
  20. local math_max = math.max
  21. local math_random = math.random
  22. dofile(pm.modpath .. "/seek.lua")
  23. dofile(pm.modpath .. "/action.lua")
  24. dofile(pm.modpath .. "/spawner.lua")
  25. dofile(pm.modpath .. "/entity.lua")
  26. function pm.target_is_player_or_mob(target)
  27. if target:is_player() then
  28. return true
  29. end
  30. local ent = target:get_luaentity()
  31. if ent.mob then
  32. return true
  33. end
  34. end
  35. function pm.debug_chat(text)
  36. -- Comment or uncomment as needed for debugging.
  37. --minetest.chat_send_all(text)
  38. --minetest.chat_send_player("MustTest", text)
  39. end
  40. function pm.debug_path(path)
  41. -- Ditto.
  42. --for k, v in ipairs(path) do pm.spawn_path_particle(v) end
  43. end
  44. function pm.debug_goal(pos)
  45. -- Ditto.
  46. --pm.spawn_path_particle(pos)
  47. --pm.death_particle_effect(pos)
  48. end
  49. function pm.death_particle_effect(pos)
  50. local particles = {
  51. amount = 100,
  52. time = 1.1,
  53. minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
  54. maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
  55. minvel = vector.new(-3.5, -3.5, -3.5),
  56. maxvel = vector.new(3.5, 3.5, 3.5),
  57. minacc = {x=0, y=0, z=0},
  58. maxacc = {x=0, y=0, z=0},
  59. minexptime = 1.5,
  60. maxexptime = 2.0,
  61. minsize = 0.5,
  62. maxsize = 1.0,
  63. collisiondetection = false,
  64. collision_removal = false,
  65. vertical = false,
  66. texture = "quartz_crystal_piece.png",
  67. glow = 14,
  68. --attached = entity,
  69. }
  70. minetest.add_particlespawner(particles)
  71. end
  72. -- Get objects inside radius, but remove self from the returned list.
  73. function pm.get_nearby_objects(self, pos, radius)
  74. local objects = minetest.get_objects_inside_radius(pos, radius)
  75. if self._identity then
  76. for i=1, #objects, 1 do
  77. local ent = objects[i]:get_luaentity()
  78. if ent and ent._identity then
  79. if ent._identity == self._identity then
  80. table.remove(objects, i)
  81. break
  82. end
  83. end
  84. end
  85. end
  86. return objects
  87. end
  88. function pm.spawn_path_particle(pos)
  89. local particles = {
  90. amount = 1,
  91. time = 0.1,
  92. minpos = pos,
  93. maxpos = pos,
  94. minvel = {x=0, y=0, z=0},
  95. maxvel = {x=0, y=0, z=0},
  96. minacc = {x=0, y=0, z=0},
  97. maxacc = {x=0, y=0, z=0},
  98. minexptime = 5,
  99. maxexptime = 5,
  100. minsize = 2.0,
  101. maxsize = 2.0,
  102. collisiondetection = false,
  103. collision_removal = false,
  104. vertical = false,
  105. texture = "default_mese_crystal.png",
  106. glow = 14,
  107. --attached = entity,
  108. }
  109. minetest.add_particlespawner(particles)
  110. end
  111. function pm.follower_spawn_particles(pos, entity)
  112. local particles = {
  113. amount = 10,
  114. time = 1,
  115. minpos = vector.add(pos, {x=-0.1, y=-0.1, z=-0.1}),
  116. maxpos = vector.add(pos, {x=0.1, y=0.1, z=0.1}),
  117. minvel = vector.new(-0.5, -0.5, -0.5),
  118. maxvel = vector.new(0.5, 0.5, 0.5),
  119. minacc = {x=0, y=0, z=0},
  120. maxacc = {x=0, y=0, z=0},
  121. minexptime = 0.5,
  122. maxexptime = 2.0,
  123. minsize = 0.5,
  124. maxsize = 1.0,
  125. collisiondetection = false,
  126. collision_removal = false,
  127. vertical = false,
  128. texture = "quartz_crystal_piece.png",
  129. glow = 14,
  130. --attached = entity,
  131. }
  132. minetest.add_particlespawner(particles)
  133. end
  134. local overrides = {
  135. nest_guard = {
  136. -- Wisp is persistent and will not alter behavior.
  137. _no_autochose_behavior = true,
  138. _no_lifespan_limit = true,
  139. },
  140. nest_worker = {
  141. -- Wisp is persistent and will not alter behavior.
  142. _no_autochose_behavior = true,
  143. _no_lifespan_limit = true,
  144. },
  145. }
  146. -- Create entity at position, if possible.
  147. function pm.spawn_wisp(pos, behavior)
  148. pos = vector_round(pos)
  149. local node = minetest.get_node(pos)
  150. local pos_ok = false
  151. if node.name == "air" then
  152. pos_ok = true
  153. else
  154. local ndef = minetest.registered_nodes[node.name]
  155. if ndef and not ndef.walkable then
  156. pos_ok = true
  157. end
  158. end
  159. if pos_ok then
  160. local ent = minetest.add_entity(pos, "pm:follower")
  161. if ent then
  162. local luaent = ent:get_luaentity()
  163. if luaent then
  164. luaent._behavior = behavior
  165. luaent._behavior_timer = math_random(1, 20)*60
  166. -- Allows to uniquely identify the wisp to other wisps, with little chance of collision.
  167. -- In particular this allows the wisp to ignore itself in any object queries.
  168. luaent._identity = math_random(1, 32000)
  169. -- This is so the wisp knows where it spawned at.
  170. -- We format it as a string so that this data is saved statically.
  171. luaent._spawn_origin = minetest.pos_to_string(pos)
  172. -- Wisp has a chance to be completely silent.
  173. if math_random(1, 10) == 1 then
  174. luaent._no_sound = true
  175. end
  176. -- Apply overrides if wanted.
  177. if overrides[behavior] then
  178. local o = overrides[behavior]
  179. for k, v in pairs(o) do
  180. luaent[k] = v
  181. end
  182. end
  183. return ent, luaent
  184. else
  185. ent:remove()
  186. end
  187. end
  188. end
  189. end
  190. local behaviors = {
  191. "follower",
  192. "pest",
  193. "thief",
  194. "healer",
  195. "explorer",
  196. "boom", -- Never chosen by chance.
  197. "communal",
  198. "solitary",
  199. "guard",
  200. "nest_guard",
  201. "arsonist",
  202. "porter",
  203. "pusher",
  204. "nest_worker",
  205. }
  206. function pm.choose_random_behavior(self)
  207. self._behavior = behaviors[math_random(1, #behaviors)]
  208. -- Don't chose a self-destructive behavior by chance.
  209. if self._behavior == "boom" then
  210. self._behavior = "follower"
  211. elseif self._behavior == "nest_guard" then
  212. self._behavior = "guard"
  213. elseif self._behavior == "nest_worker" then
  214. self._behavior = "explorer"
  215. end
  216. end
  217. -- Create entity at position, if possible.
  218. function pm.spawn_random_wisp(pos)
  219. local act = behaviors[math_random(1, #behaviors)]
  220. if act == "boom" then
  221. act = "follower"
  222. elseif act == "nest_guard" then
  223. act = "guard"
  224. elseif act == "nest_worker" then
  225. act = "explorer"
  226. end
  227. return pm.spawn_wisp(pos, act)
  228. end
  229. -- Table of functions for obtaining interest points.
  230. local interests = {
  231. follower = function(self, pos)
  232. return pm.seek_player_or_mob_or_item(self, pos)
  233. end,
  234. thief = function(self, pos)
  235. return pm.seek_player_or_item(self, pos)
  236. end,
  237. pest = function(self, pos)
  238. return pm.seek_player(self, pos)
  239. end,
  240. healer = function(self, pos)
  241. return pm.seek_player(self, pos)
  242. end,
  243. explorer = function(self, pos)
  244. return pm.seek_node_with_meta(self, pos)
  245. end,
  246. -- Suicide, never chosen at random.
  247. boom = function(self, pos)
  248. return pm.seek_player_or_mob_not_wisp(self, pos)
  249. end,
  250. communal = function(self, pos)
  251. return pm.seek_wisp(self, pos)
  252. end,
  253. solitary = function(self, pos)
  254. return pm.seek_solitude(self, pos)
  255. end,
  256. guard = function(self, pos)
  257. -- Seek target in sight-range of spawn origin, otherwise return to origin.
  258. if self._spawn_origin then
  259. local origin = minetest.string_to_pos(self._spawn_origin)
  260. if origin then
  261. if vector_distance(origin, self.object:get_pos()) > pm.sight_range then
  262. return origin, nil
  263. else
  264. -- Within sight range of spawn origin, seek target.
  265. return pm.seek_player_or_mob_not_wisp(self, pos)
  266. end
  267. end
  268. end
  269. return nil, nil
  270. end,
  271. -- Never chosen at random, can only be deliberately created.
  272. nest_guard = function(self, pos)
  273. -- Seek target in sight-range of spawn origin, otherwise return to origin.
  274. if self._spawn_origin then
  275. local origin = minetest.string_to_pos(self._spawn_origin)
  276. if origin then
  277. if vector_distance(origin, self.object:get_pos()) > pm.nest_range then
  278. return origin, nil
  279. else
  280. -- Within sight range of spawn origin, seek target.
  281. return pm.seek_player(self, pos)
  282. end
  283. end
  284. end
  285. return nil, nil
  286. end,
  287. -- Never chosen at random, can only be deliberately created.
  288. nest_worker = function(self, pos)
  289. -- Seek target in sight-range of spawn origin, otherwise return to origin.
  290. if self._spawn_origin then
  291. local origin = minetest.string_to_pos(self._spawn_origin)
  292. if origin then
  293. if vector_distance(origin, self.object:get_pos()) > pm.sight_range then
  294. return origin, nil
  295. else
  296. -- Within sight range of spawn origin, seek target.
  297. return pm.seek_flora(self, pos)
  298. end
  299. end
  300. end
  301. return nil, nil
  302. end,
  303. arsonist = function(self, pos)
  304. local target = pm.seek_flammable_node(self, pos)
  305. if not target then
  306. return pm.seek_player_or_mob(self, pos)
  307. end
  308. return target, nil
  309. end,
  310. porter = function(self, pos)
  311. return pm.seek_player(self, pos)
  312. end,
  313. pusher = function(self, pos)
  314. return pm.seek_player(self, pos)
  315. end,
  316. }
  317. -- Table of possible action functions to take on arriving at a target.
  318. local actions = {
  319. pest = function(self, pos, target)
  320. pm.hurt_nearby_players(self)
  321. end,
  322. healer = function(self, pos, target)
  323. pm.heal_nearby_players(self)
  324. end,
  325. thief = function(self, pos, target)
  326. pm.steal_nearby_item(self, target)
  327. end,
  328. boom = function(self, pos, target)
  329. pm.explode_nearby_target(self, target)
  330. end,
  331. guard = function(self, pos, target)
  332. -- Attack target, but only if in sight-range of spawn origin.
  333. if self._spawn_origin then
  334. local origin = minetest.string_to_pos(self._spawn_origin)
  335. if origin then
  336. if vector_distance(origin, self.object:get_pos()) < pm.sight_range then
  337. pm.hurt_nearby_player_or_mob_not_wisp(self)
  338. end
  339. end
  340. end
  341. end,
  342. nest_guard = function(self, pos, target)
  343. -- Attack target, but only if in sight-range of spawn origin.
  344. if self._spawn_origin then
  345. local origin = minetest.string_to_pos(self._spawn_origin)
  346. if origin then
  347. if vector_distance(origin, self.object:get_pos()) < pm.nest_range then
  348. pm.hurt_nearby_player_or_mob_not_wisp(self)
  349. end
  350. end
  351. end
  352. end,
  353. arsonist = function(self, pos, target)
  354. pm.commit_arson_at_target(pos)
  355. end,
  356. porter = function(self, pos, target)
  357. pm.teleport_player_to_prior_location(target)
  358. end,
  359. pusher = function(self, pos, target)
  360. pm.shove_player(self, target)
  361. end,
  362. }
  363. function pm.interest_point(self, pos)
  364. if self._behavior then
  365. if interests[self._behavior] then
  366. return interests[self._behavior](self, pos)
  367. end
  368. end
  369. return nil, nil
  370. end
  371. function pm.on_arrival(self, pos, other)
  372. pm.debug_chat('arrived at target')
  373. if self._behavior then
  374. pm.debug_chat('have behavior: ' .. self._behavior)
  375. if actions[self._behavior] then
  376. actions[self._behavior](self, pos, other)
  377. end
  378. end
  379. end
  380. if not pm.registered then
  381. local entity = {
  382. initial_properties = {
  383. visual = "cube",
  384. textures = {
  385. "quartz_crystal_piece.png",
  386. "quartz_crystal_piece.png",
  387. "quartz_crystal_piece.png",
  388. "quartz_crystal_piece.png",
  389. "quartz_crystal_piece.png",
  390. "quartz_crystal_piece.png",
  391. },
  392. visual_size = {x=0.2, y=0.2, z=0.2},
  393. collide_with_objects = false,
  394. pointable = false,
  395. is_visible = true,
  396. makes_footstep_sound = false,
  397. glow = 14,
  398. automatic_rotate = 0.5,
  399. -- This is so that wandering/drifting wisps don't bury themselves.
  400. physical = true,
  401. collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2},
  402. },
  403. -- So other game code can tell what this entity is.
  404. _name = "pm:follower",
  405. description = "Seon",
  406. mob = true,
  407. on_step = function(...) return pm.follower_on_step(...) end,
  408. on_punch = function(...) return pm.follower_on_punch(...) end,
  409. on_activate = function(...) return pm.follower_on_activate(...) end,
  410. get_staticdata = function(...) return pm.follower_get_staticdata(...) end,
  411. _interest_point = function(...) return pm.interest_point(...) end,
  412. _on_arrival = function(...) return pm.on_arrival(...) end,
  413. }
  414. minetest.register_entity("pm:follower", entity)
  415. minetest.register_node("pm:spawner", {
  416. drawtype = "airlike",
  417. description = "Wisp Spawner (Please Report to Admin)",
  418. paramtype = "light",
  419. sunlight_propagates = true,
  420. walkable = false,
  421. pointable = false,
  422. groups = {immovable = 1},
  423. climbable = false,
  424. buildable_to = true,
  425. floodable = true,
  426. drop = "",
  427. on_construct = function(...)
  428. return pm.on_nodespawner_construct(...)
  429. end,
  430. on_destruct = function(...)
  431. return pm.on_nodespawner_destruct(...)
  432. end,
  433. on_timer = function(...)
  434. return pm.on_nodespawner_timer(...)
  435. end,
  436. on_finish_collapse = function(pos, node)
  437. minetest.remove_node(pos)
  438. end,
  439. on_collapse_to_entity = function(pos, node)
  440. -- Do nothing.
  441. end,
  442. })
  443. minetest.register_node("pm:quartz_ore", {
  444. description = "Quartz Crystals In Sand",
  445. tiles = {"default_desert_sand.png^quartz_ore.png"},
  446. groups = utility.dig_groups("mineral"),
  447. drop = 'quartz:quartz_crystal',
  448. sounds = default.node_sound_stone_defaults(),
  449. silverpick_drop = true,
  450. movement_speed_multiplier = default.SLOW_SPEED,
  451. after_destruct = function(pos, oldnode)
  452. if math_random(1, 500) == 1 then
  453. pm.spawn_random_wisp(pos)
  454. end
  455. end,
  456. })
  457. local c = "pm:core"
  458. local f = pm.modpath .. "/init.lua"
  459. reload.register_file(c, f, false)
  460. pm.registered = true
  461. end