init.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. radiant_damage = {} --create a container for functions and constants
  2. local modpath = minetest.get_modpath(minetest.get_current_modname())
  3. dofile(modpath.."/config.lua")
  4. -- damage_def:
  5. --{
  6. -- interval = 1, -- number of seconds between each damage check
  7. -- range = 3, -- range of the damage. Can be omitted if inverse_square_falloff is true, in that case it defaults to the range at which 1 point of damage is done by the most damaging emitter node type.
  8. -- emitted_by = {}, -- nodes that emit this damage. At least one is required.
  9. -- attenuated_by = {} -- This allows certain intervening node types to modify the damage that radiates through it. Note: Only works in Minetest version 0.5 and above.
  10. -- default_attenuation = 1, -- the amount the damage is multiplied by when passing through any other non-air nodes. Note that in versions before Minetest 0.5 any value other than 1 will result in total occlusion (ie, any non-air node will block all damage)
  11. -- inverse_square_falloff = true, -- if true, damage falls off with the inverse square of the distance. If false, damage is constant within the range.
  12. -- above_only = false, -- if true, damage only propagates directly upward. Useful for when you want to damage players that stand on the node.
  13. -- on_damage = function(player_object, damage_value, pos) -- An optional callback to allow mods to do custom behaviour. If this is set to non-nil then the default damage will *not* be done to the player, it's up to the callback to handle that.
  14. --}
  15. -- emitted_by has the following format:
  16. -- {["default:stone_with_mese"] = 2, ["default:mese"] = 9}
  17. -- where the value associated with each entry is the amount of damage dealt. Groups are permitted. Note that negative damage represents "healing" radiation.
  18. -- attenuated_by has the following similar format:
  19. -- {["group:stone"] = 0.25, ["default:steelblock"] = 0}
  20. -- where the value is a multiplier that is applied to the damage passing through it. Groups are permitted. Note that you can use values greater than one to make a node type magnify damage instead of attenuating it.
  21. -- Commmon function for looking up an emitted_by or attenuated_by value for a node
  22. local get_val = function(node_name, target_names, target_groups)
  23. if target_names then
  24. local name_val = target_names[node_name]
  25. if name_val ~= nil then return name_val end
  26. end
  27. if target_groups then
  28. local node_def = minetest.registered_nodes[node_name]
  29. local node_groups = node_def.groups
  30. if node_groups then
  31. for group, _ in pairs(node_groups) do
  32. local group_val = target_groups[group]
  33. if group_val ~= nil then return group_val end -- returns the first group value it finds, if multiple apply it's undefined which will be selected
  34. end
  35. end
  36. end
  37. return nil
  38. end
  39. local attenuation_check
  40. if Raycast ~= nil then -- version 0.5 of Minetest adds the Raycast class, use that.
  41. -- Gets three raycasts from the faces of the nodes facing the player.
  42. local get_raycasts = function(node_pos, player_pos)
  43. local results = {}
  44. if player_pos.x > node_pos.x then
  45. table.insert(results, Raycast({x=node_pos.x+0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
  46. else
  47. table.insert(results, Raycast({x=node_pos.x-0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
  48. end
  49. if player_pos.y > node_pos.y then
  50. table.insert(results, Raycast({y=node_pos.y+0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
  51. else
  52. table.insert(results, Raycast({y=node_pos.y-0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
  53. end
  54. if player_pos.z > node_pos.z then
  55. table.insert(results, Raycast({z=node_pos.z+0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
  56. else
  57. table.insert(results, Raycast({z=node_pos.z-0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
  58. end
  59. return results
  60. end
  61. attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
  62. -- First check a simple degenerate case; if there are no special modifier nodes and the default attenuation
  63. -- is 1 then we don't need to bother with any detailed checking, the damage goes through unmodified.
  64. if default_attenuation == 1 and attenuation_nodes == nil and attenuation_groups == nil then return 1 end
  65. local raycasts = get_raycasts(node_pos, player_pos)
  66. local farthest_from_zero = 0
  67. for _, raycast in pairs(raycasts) do
  68. local current_attenuation = 1
  69. for ray_node in raycast do
  70. local ray_node_name = minetest.get_node(ray_node.under).name
  71. local ray_node_val = get_val(ray_node_name, attenuation_nodes, attenuation_groups)
  72. if ray_node_val == nil then ray_node_val = default_attenuation end
  73. current_attenuation = current_attenuation * ray_node_val
  74. if current_attenuation == 0 then break end -- once we hit zero no further checks are needed, it will never change.
  75. end
  76. -- By always selecting the farthest value from zero we accomodate both "healing" and "harmful" radiation
  77. -- and always let the most impactful value of either type through.
  78. -- If you've got both positive and negative modifiers (for example, if you've got a magical node that turns
  79. -- harmful radiation into healing radiation when it passes through) this could result in somewhat erratic effects.
  80. -- But that's part of the fun, eh? Players will just need to design and use their healing ray carefully.
  81. if math.abs(current_attenuation) > math.abs(farthest_from_zero) then
  82. farthest_from_zero = current_attenuation
  83. end
  84. end
  85. return farthest_from_zero
  86. end
  87. else
  88. -- Pre-Minetest 0.5 version. Attenuation_nodes and attenuation_groups are ignored
  89. attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
  90. if default_attenuation == 1 then return 1 end -- if default_attenuation is 1, don't attenuate.
  91. -- otherwise, it's all-or-nothing:
  92. if player_pos.y > node_pos.y then
  93. if minetest.line_of_sight({y=node_pos.y+0.51, x=node_pos.x, z=node_pos.z}, player_pos) then return 1 end
  94. else
  95. if minetest.line_of_sight({y=node_pos.y-0.51, x=node_pos.x, z=node_pos.z}, player_pos) then return 1 end
  96. end
  97. if player_pos.x > node_pos.x then
  98. if minetest.line_of_sight({x=node_pos.x+0.51, y=node_pos.y, z=node_pos.z}, player_pos) then return 1 end
  99. else
  100. if minetest.line_of_sight({x=node_pos.x-0.51, y=node_pos.y, z=node_pos.z}, player_pos) then return 1 end
  101. end
  102. if player_pos.z > node_pos.z then
  103. if minetest.line_of_sight({z=node_pos.z+0.51, x=node_pos.x, y=node_pos.y}, player_pos) then return 1 end
  104. else
  105. if minetest.line_of_sight({z=node_pos.z-0.51, x=node_pos.x, y=node_pos.y}, player_pos) then return 1 end
  106. end
  107. return 0
  108. end
  109. end
  110. radiant_damage.registered_damage_types = {}
  111. -- This method will update the registered_damage_types table with new values, keeping all the parameters
  112. -- consistent and in proper relation to each other.
  113. local update_damage_type = function(damage_name, new_def)
  114. if radiant_damage.registered_damage_types[damage_name] == nil then
  115. radiant_damage.registered_damage_types[damage_name] = {}
  116. end
  117. local damage_def = radiant_damage.registered_damage_types[damage_name]
  118. -- Interval
  119. if new_def.interval ~= nil then
  120. damage_def.interval = new_def.interval
  121. elseif damage_def.interval == nil then
  122. damage_def.interval = 1
  123. end
  124. -- Inverse square falloff
  125. if new_def.inverse_square_falloff ~= nil then
  126. damage_def.inverse_square_falloff = new_def.inverse_square_falloff
  127. elseif damage_def.inverse_square_falloff == nil then
  128. damage_def.inverse_square_falloff = true
  129. end
  130. -- Default attenuation value
  131. if new_def.default_attenuation ~= nil then
  132. damage_def.default_attenuation = new_def.default_attenuation
  133. elseif damage_def.default_attenuation == nil then
  134. damage_def.default_attenuation = 0
  135. end
  136. -- Above Only
  137. damage_def.above_only = new_def.above_only -- default to false
  138. -- on_damage callback
  139. damage_def.on_damage = new_def.on_damage
  140. -- it is efficient to split the emission and attenuation data into separate node and group maps.
  141. -- Emitted by
  142. damage_def.emission_nodes = damage_def.emission_nodes or {}
  143. damage_def.emission_groups = damage_def.emission_groups or {}
  144. for nodename, damage in pairs(new_def.emitted_by) do
  145. if damage == 0 then damage = nil end -- causes removal of damage-0 node types
  146. if string.sub(nodename, 1, 6) == "group:" then
  147. damage_def.emission_groups[string.sub(nodename, 7)] = damage -- omit the "group:" prefix
  148. else
  149. damage_def.emission_nodes[nodename] = damage
  150. end
  151. end
  152. damage_def.nodenames = damage_def.nodenames or {} -- for use with minetest.find_nodes_in_area
  153. for nodename, damage in pairs(new_def.emitted_by) do
  154. local handled = false
  155. for i, v in ipairs(damage_def.nodenames) do
  156. if v == nodename then
  157. if damage == 0 then
  158. table.remove(damage_def.nodenames, i)
  159. end
  160. handled = true
  161. break
  162. end
  163. end
  164. if not handled then
  165. table.insert(damage_def.nodenames, nodename)
  166. end
  167. end
  168. -- These remain nil unless some valid data is provided.
  169. if new_def.attenuated_by and Raycast then
  170. for nodename, attenuation in pairs(new_def.attenuated_by) do
  171. damage_def.attenuation_nodes = damage_def.attenuation_nodes or {}
  172. damage_def.attenuation_groups = damage_def.attenuation_groups or {}
  173. if string.sub(nodename, 1, 6) == "group:" then
  174. damage_def.attenuation_groups[string.sub(nodename, 7)] = attenuation -- omit the "group:" prefix
  175. else
  176. damage_def.attenuation_nodes[nodename] = attenuation
  177. end
  178. end
  179. end
  180. -- remove any attenuation nodes or groups that match the default attenuation, they're pointless.
  181. if damage_def.attenuation_groups then
  182. for node, attenuation in pairs(damage_def.attenuation_groups) do
  183. if attenuation == damage_def.default_attenuation then
  184. damage_def.attenuation_groups[node] = nil
  185. end
  186. end
  187. end
  188. if damage_def.attenuation_nodes then
  189. for node, attenuation in pairs(damage_def.attenuation_nodes) do
  190. if attenuation == damage_def.default_attenuation then
  191. damage_def.attenuation_nodes[node] = nil
  192. end
  193. end
  194. end
  195. -- Range
  196. if new_def.range ~= nil then
  197. damage_def.absolute_range = true
  198. damage_def.range = new_def.range
  199. elseif damage_def.inverse_square_falloff and not damage_def.absolute_range then
  200. damage_def.range = 0
  201. for _, damage in pairs(damage_def.emission_nodes) do
  202. damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
  203. end
  204. for _, damage in pairs(damage_def.emission_groups) do
  205. damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
  206. end
  207. end
  208. return damage_def
  209. end
  210. radiant_damage.override_radiant_damage = function(damage_name, damage_def)
  211. if radiant_damage.registered_damage_types[damage_name] then
  212. update_damage_type(damage_name, damage_def)
  213. elseif minetest.settings:get_bool("enable_damage") then
  214. minetest.log("error", "Attempt was made to override unregistered radiant_damage type " .. damage_name)
  215. end
  216. end
  217. radiant_damage.register_radiant_damage = function(damage_name, damage_def)
  218. if not minetest.settings:get_bool("enable_damage") then return end -- don't bother if enable_damage isn't set.
  219. if radiant_damage.registered_damage_types[damage_name] then
  220. minetest.log("error", "Attempt was made to register the already-registered radiant_damage type " .. damage_name)
  221. return
  222. end
  223. local damage_def = update_damage_type(damage_name, damage_def)
  224. local range = damage_def.range
  225. local above_only = damage_def.above_only
  226. local nodenames = damage_def.nodenames
  227. local default_attenuation = damage_def.default_attenuation
  228. local attenuation_nodes = damage_def.attenuation_nodes
  229. local attenuation_groups = damage_def.attenuation_groups
  230. local emission_nodes = damage_def.emission_nodes
  231. local emission_groups = damage_def.emission_groups
  232. local inverse_square_falloff = damage_def.inverse_square_falloff
  233. local on_damage = damage_def.on_damage
  234. local interval = damage_def.interval
  235. local timer = 0
  236. minetest.register_globalstep(function(dtime)
  237. timer = timer + dtime
  238. if timer >= interval then
  239. timer = timer - interval
  240. for _, player in pairs(minetest.get_connected_players()) do
  241. local player_pos = player:get_pos() -- node player's feet are in this location. Add 1 to y to get chest height, more intuitive that way
  242. player_pos.y = player_pos.y + 1
  243. local rounded_pos = vector.round(player_pos)
  244. local nearby_nodes
  245. if above_only then
  246. nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, {x=0, y= -range, z=0}), rounded_pos, nodenames)
  247. else
  248. nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, -range), vector.add(rounded_pos, range), nodenames)
  249. end
  250. local total_damage = 0
  251. for _, node_pos in ipairs(nearby_nodes) do
  252. local distance
  253. if above_only then
  254. distance = math.max(player_pos.y - node_pos.y, 1)
  255. else
  256. distance = math.max(vector.distance(player_pos, node_pos), 1) -- clamp to 1 to avoid inverse falloff causing crazy huge damage when standing inside a node
  257. end
  258. if distance <= range then
  259. local attenuation = attenuation_check(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
  260. if attenuation ~= 0 then
  261. local damage = get_val(minetest.get_node(node_pos).name, emission_nodes, emission_groups)
  262. if inverse_square_falloff then
  263. total_damage = total_damage + (damage / (distance * distance)) * attenuation
  264. else
  265. total_damage = total_damage + damage * attenuation
  266. end
  267. end
  268. end
  269. end
  270. if on_damage == nil then
  271. total_damage = math.floor(total_damage)
  272. if total_damage ~= 0 then
  273. minetest.log("action", player:get_player_name() .. " takes " .. tostring(total_damage) .. " damage from " .. damage_name .. " radiant damage at " .. minetest.pos_to_string(rounded_pos))
  274. player:set_hp(player:get_hp() - total_damage)
  275. end
  276. else
  277. on_damage(player, total_damage, rounded_pos)
  278. end
  279. end
  280. end
  281. end)
  282. end
  283. if radiant_damage.config.enable_heat_damage then
  284. local on_fire_damage
  285. if minetest.get_modpath("3d_armor") and armor ~= nil then
  286. -- 3d_armor uses a strange fire protection system different from all the rest, wherein
  287. -- its armor protects wholly against some heat sources and not at all against others
  288. -- based on how "hot" they are and how strong against fire the armor is.
  289. -- Level 1 protects against a wall torch, level 3 protects against a basic fire, and level 5 protects against lava.
  290. -- Converting this into a standard armor type is going to require some arbitrary decisions.
  291. -- My decision is: level 5 protection should reduce the default damage from lava immersion from 8 to 0.5 hp (0.0625 multiplier).
  292. -- Level 3 protection reduces the default damage from basic flame from 4 to 0.5 hp (0.125 multiplier)
  293. -- Torches don't do damage in default so I will ignore that.
  294. -- Level 0 has a damage multiplier of 1.
  295. -- That gives us three data points: (0,1), (3,0.125), (5,0.0625). Fitting that to an exponential curve gives us:
  296. -- y = 0.0481417 + 0.9518583*e^(-0.8388176*x)
  297. -- Which looks about right on a graph, and "looks about right on a graph" is good enough for me.
  298. on_fire_damage = function(player, damage, pos)
  299. local fire_protection = armor.def[player:get_player_name()].fire
  300. local fire_multiplier = 1
  301. if fire_protection then
  302. fire_multiplier = math.min(1, 0.0481417 + 0.9518583 * math.exp(-0.8388176*fire_protection))
  303. end
  304. -- If the player also has conventional fire armor, use whatever's better.
  305. local fire_armor = player:get_armor_groups().fire
  306. if fire_armor then
  307. fire_multiplier = math.min(fire_multiplier, fire_armor/100)
  308. end
  309. damage = math.floor(damage * fire_multiplier)
  310. if damage > 0 then
  311. minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
  312. player:set_hp(player:get_hp() - damage)
  313. minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
  314. end
  315. end
  316. else
  317. on_fire_damage = function(player, damage, pos)
  318. local fire_armor = player:get_armor_groups().fire
  319. if fire_armor then
  320. damage = damage * fire_armor / 100
  321. end
  322. damage = math.floor(damage)
  323. if damage > 0 then
  324. minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
  325. player:set_hp(player:get_hp() - damage)
  326. minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
  327. end
  328. end
  329. end
  330. radiant_damage.register_radiant_damage("heat", {
  331. interval = 1,
  332. emitted_by = {["group:lava"] = radiant_damage.config.lava_damage, ["fire:basic_flame"] = radiant_damage.config.fire_damage,
  333. ["fire:permanent_flame"] = radiant_damage.config.fire_damage, ['nether:lava_crust'] = .25,},
  334. inverse_square_falloff = true,
  335. default_attenuation = 0, -- heat is blocked by anything.
  336. on_damage = on_fire_damage,
  337. })
  338. end
  339. if radiant_damage.config.enable_mese_damage then
  340. local shields = {"default:steelblock", "default:copperblock", "default:tinblock", "default:bronzeblock", "default:goldblock"}
  341. local amplifiers = {"default:diamondblock", "default:coalblock"}
  342. for _, shielding_node in ipairs(shields) do
  343. local node_def = minetest.registered_nodes[shielding_node]
  344. if node_def then
  345. local new_groups = node_def.groups or {}
  346. new_groups.mese_radiation_shield = 1
  347. minetest.override_item(shielding_node, {groups=new_groups})
  348. end
  349. end
  350. for _, amp_node in ipairs(amplifiers) do
  351. local node_def = minetest.registered_nodes[amp_node]
  352. if node_def then
  353. local new_groups = node_def.groups or {}
  354. new_groups.mese_radiation_amplifier = 1
  355. minetest.override_item(amp_node, {groups=new_groups})
  356. end
  357. end
  358. local on_radiation_damage = function(player, damage, pos)
  359. local radiation_multiplier = player:get_armor_groups().radiation
  360. if radiation_multiplier then
  361. damage = damage * radiation_multiplier / 100
  362. end
  363. damage = math.floor(damage)
  364. if damage > 0 then
  365. minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from mese radiation damage at " .. minetest.pos_to_string(pos))
  366. player:set_hp(player:get_hp() - damage)
  367. minetest.sound_play({name = "radiant_damage_geiger", gain = math.min(1, damage/10)}, {to_player=player:get_player_name()})
  368. end
  369. end
  370. radiant_damage.register_radiant_damage("mese", {
  371. interval = radiant_damage.config.mese_interval,
  372. inverse_square_falloff = true,
  373. emitted_by = {["default:stone_with_mese"] = radiant_damage.config.mese_damage, ["default:mese"] = radiant_damage.config.mese_damage * 9},
  374. attenuated_by = {["group:stone"] = 0.5, ["group:mese_radiation_shield"] = 0.1, ["group:mese_radiation_amplifier"] = 4},
  375. default_attenuation = 0.9,
  376. on_damage = on_radiation_damage,
  377. })
  378. end