director.lua 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. defense.director = {}
  2. local director = defense.director
  3. director.update_interval = 1.0
  4. director.intensity_decay = 0.93
  5. director.max_entities = 50
  6. --[[
  7. spawn_list: List of spawn events that may happen
  8. description: Description for this spawn event (mostly for debugging)
  9. name: Name of the entity to spawn
  10. intensity_min: Minimum intensity requirement for the event to fire
  11. intensity_max: Maximum intensity requirement for the event to fire
  12. group_min: Minimum number of entities to spawn when event fires
  13. group_max: Maximum number of entities to spawn when event fires
  14. probability: Probability of spawning per update (excluding cooldown times)
  15. day_start: Number of game days before this event can start happening
  16. spawn_time: Spawn cooldown
  17. spawn_location_type: ["ground"/"air"] Where entities will appear
  18. ]]
  19. director.spawn_list = {
  20. {
  21. description = "Unggoy group",
  22. name = "defense:unggoy",
  23. intensity_min = 0.0,
  24. intensity_max = 0.6,
  25. group_min = 1,
  26. group_max = 4,
  27. probability = 0.4,
  28. day_start = 0,
  29. spawn_time = 14.0,
  30. spawn_location_type = "ground",
  31. },
  32. {
  33. description = "Unggoy horde",
  34. name = "defense:unggoy",
  35. intensity_min = 0.0,
  36. intensity_max = 0.1,
  37. group_min = 21,
  38. group_max = 24,
  39. probability = 0.8,
  40. day_start = 1,
  41. spawn_time = 31.0,
  42. spawn_location_type = "ground",
  43. },
  44. {
  45. description = "Paniki group",
  46. name = "defense:paniki",
  47. intensity_min = 0.0,
  48. intensity_max = 0.3,
  49. group_min = 1,
  50. group_max = 6,
  51. probability = 0.6,
  52. day_start = 0,
  53. spawn_time = 9.0,
  54. spawn_location_type = "air",
  55. },
  56. {
  57. description = "Sarangay",
  58. name = "defense:sarangay",
  59. intensity_min = 0.0,
  60. intensity_max = 0.2,
  61. group_min = 1,
  62. group_max = 1,
  63. probability = 0.4,
  64. day_start = 2,
  65. spawn_time = 90.0,
  66. spawn_location_type = "ground",
  67. },
  68. {
  69. description = "Botete",
  70. name = "defense:botete",
  71. intensity_min = 0.0,
  72. intensity_max = 0.3,
  73. group_min = 1,
  74. group_max = 1,
  75. probability = 0.4,
  76. day_start = 1,
  77. spawn_time = 90.0,
  78. spawn_location_type = "air",
  79. },
  80. }
  81. -- State tracking stuff
  82. director.intensity = 0.5
  83. director.cooldown_timer = 3
  84. local spawn_timers = {}
  85. local last_average_health = 1.0
  86. local last_mob_count = 0
  87. for _,m in ipairs(director.spawn_list) do
  88. spawn_timers[m.description] = m.spawn_time/2
  89. end
  90. local function find_spawn_position(spawn_location_type)
  91. local players = minetest.get_connected_players()
  92. if #players == 0 then
  93. return nil
  94. end
  95. local center = {x=0, y=0, z=0}
  96. for _,p in ipairs(players) do
  97. center = vector.add(center, p:get_pos())
  98. end
  99. center = vector.multiply(center, #players)
  100. local radius = {}
  101. local points = {}
  102. for _,p in ipairs(players) do
  103. local r = 20 + 10/(vector.distance(p:get_pos(), center) + 1)
  104. radius[p:get_player_name()] = r - 0.5
  105. for j = 0, 3, 1 do
  106. local pos = p:get_pos()
  107. local a = math.random() * 2 * math.pi
  108. pos.x = pos.x + math.cos(a) * r
  109. pos.z = pos.z + math.sin(a) * r
  110. if spawn_location_type == "ground" then
  111. -- Move pos to on ground
  112. pos.y = pos.y + 10
  113. local d = -1
  114. local node = minetest.get_node_or_nil(pos)
  115. if node and minetest.registered_nodes[node.name].walkable then
  116. d = 1
  117. pos.y = pos.y + 1
  118. end
  119. for i = pos.y, pos.y + 40 * d, d do
  120. local top = {x=pos.x, y=i, z=pos.z}
  121. local bottom = {x=pos.x, y=i-1, z=pos.z}
  122. local node_top = minetest.get_node_or_nil(top)
  123. local node_bottom = minetest.get_node_or_nil(bottom)
  124. if node_bottom and node_top
  125. and minetest.registered_nodes[node_bottom.name].walkable ~= minetest.registered_nodes[node_top.name].walkable then
  126. table.insert(points, top)
  127. break
  128. end
  129. end
  130. elseif spawn_location_type == "air" then
  131. -- Move pos up
  132. pos.y = pos.y + 12 + math.random() * 12
  133. local node = minetest.get_node_or_nil(pos)
  134. if node and not minetest.registered_nodes[node.name].walkable then
  135. table.insert(points, pos)
  136. end
  137. end
  138. end
  139. end
  140. if #points == 0 then
  141. return nil
  142. end
  143. local filtered = {}
  144. for _,p in ipairs(players) do
  145. local pos = p:get_pos()
  146. for _,o in ipairs(points) do
  147. if vector.distance(pos, o) >= radius[p:get_player_name()] then
  148. table.insert(filtered, o)
  149. end
  150. end
  151. end
  152. if #filtered > 0 then
  153. return filtered[math.random(#filtered)]
  154. end
  155. return nil
  156. end
  157. local function spawn_monsters()
  158. -- Filter eligible monsters
  159. local filtered = {}
  160. for _,m in ipairs(director.spawn_list) do
  161. if spawn_timers[m.description] <= 0
  162. and defense.get_day_count() >= m.day_start
  163. and math.random() < m.probability
  164. and director.intensity >= m.intensity_min
  165. and director.intensity <= m.intensity_max then
  166. table.insert(filtered, m)
  167. end
  168. end
  169. if #filtered == 0 then
  170. return false
  171. end
  172. local monster = filtered[math.random(#filtered)]
  173. -- Determine group size
  174. local intr = math.max(0, math.min(1, director.intensity + math.random() * 2 - 1))
  175. local group_size = math.floor(0.5 + monster.group_max + (monster.group_min - monster.group_max) * intr)
  176. -- Find the spawn position
  177. local pos = find_spawn_position(monster.spawn_location_type)
  178. if not pos then
  179. defense:log("No spawn point found for " .. monster.description .. "!")
  180. return false
  181. end
  182. -- Spawn
  183. defense:log("Spawn " .. monster.description .. " (" .. group_size .. " " .. monster.name .. ") at " .. minetest.pos_to_string(pos))
  184. repeat
  185. minetest.after(group_size * (math.random() * 0.2), function()
  186. local obj = minetest.add_entity(pos, monster.name)
  187. end)
  188. group_size = group_size - 1
  189. until group_size <= 0
  190. spawn_timers[monster.description] = monster.spawn_time
  191. return true
  192. end
  193. local function update_intensity()
  194. local players = minetest.get_connected_players()
  195. if #players == 0 then
  196. return
  197. end
  198. local average_health = 0
  199. for _,p in ipairs(players) do
  200. average_health = average_health + p:get_hp()
  201. end
  202. average_health = average_health / #players
  203. local mob_count = #minetest.luaentities
  204. local delta =
  205. -0.2 * (average_health - last_average_health)
  206. + 4.0 * math.max(0, 1 / average_health - 0.1)
  207. + 0.006 * (mob_count - last_mob_count)
  208. last_average_health = average_health
  209. last_mob_count = mob_count
  210. director.intensity = math.max(0, math.min(1, director.intensity * director.intensity_decay + delta))
  211. end
  212. local function update()
  213. update_intensity()
  214. if director.cooldown_timer <= 0 then
  215. if defense:is_dark() and #minetest.luaentities < director.max_entities and not defense.debug then
  216. spawn_monsters()
  217. end
  218. if director.intensity > 0.5 then
  219. director.cooldown_timer = math.random(5, 5 + 80 * (director.intensity - 0.5))
  220. end
  221. else
  222. director.cooldown_timer = director.cooldown_timer - director.update_interval
  223. end
  224. for k,v in pairs(spawn_timers) do
  225. if v > 0 then
  226. spawn_timers[k] = v - director.update_interval
  227. end
  228. end
  229. end
  230. local function save()
  231. local data = {
  232. intensity = director.intensity,
  233. cooldown_timer = director.cooldown_timer,
  234. spawn_timers = spawn_timers,
  235. last_average_health = last_average_health,
  236. last_mob_count = last_mob_count,
  237. }
  238. defense.storage:set_string("director", minetest.serialize(data))
  239. end
  240. local function load()
  241. local director_data = defense.storage:get_string("director")
  242. if director_data ~= ""
  243. then
  244. local data = minetest.deserialize(director_data)
  245. director.intensity = data.intensity
  246. director.cooldown_timer = data.cooldown_timer
  247. last_average_health = data.last_average_health
  248. last_mob_count = data.last_mob_count
  249. for k,v in pairs(data.spawn_timers) do
  250. spawn_timers[k] = v
  251. end
  252. end
  253. end
  254. minetest.register_on_shutdown(function()
  255. save()
  256. end)
  257. load()
  258. local last_update_time = 0
  259. minetest.register_globalstep(function(dtime)
  260. local gt = minetest.get_gametime()
  261. if last_update_time + director.update_interval < gt then
  262. update()
  263. last_update_time = gt
  264. end
  265. end)