123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- radiant_damage = {} --create a container for functions and constants
- local modpath = minetest.get_modpath(minetest.get_current_modname())
- dofile(modpath.."/config.lua")
- -- damage_def:
- --{
- -- interval = 1, -- number of seconds between each damage check
- -- 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.
- -- emitted_by = {}, -- nodes that emit this damage. At least one is required.
- -- 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.
- -- 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)
- -- inverse_square_falloff = true, -- if true, damage falls off with the inverse square of the distance. If false, damage is constant within the range.
- -- above_only = false, -- if true, damage only propagates directly upward. Useful for when you want to damage players that stand on the node.
- -- 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.
- --}
- -- emitted_by has the following format:
- -- {["default:stone_with_mese"] = 2, ["default:mese"] = 9}
- -- where the value associated with each entry is the amount of damage dealt. Groups are permitted. Note that negative damage represents "healing" radiation.
- -- attenuated_by has the following similar format:
- -- {["group:stone"] = 0.25, ["default:steelblock"] = 0}
- -- 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.
- -- Commmon function for looking up an emitted_by or attenuated_by value for a node
- local get_val = function(node_name, target_names, target_groups)
- if target_names then
- local name_val = target_names[node_name]
- if name_val ~= nil then return name_val end
- end
- if target_groups then
- local node_def = minetest.registered_nodes[node_name]
- local node_groups = node_def.groups
- if node_groups then
- for group, _ in pairs(node_groups) do
- local group_val = target_groups[group]
- 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
- end
- end
- end
- return nil
- end
- local attenuation_check
- if Raycast ~= nil then -- version 0.5 of Minetest adds the Raycast class, use that.
- -- Gets three raycasts from the faces of the nodes facing the player.
- local get_raycasts = function(node_pos, player_pos)
- local results = {}
- if player_pos.x > node_pos.x then
- table.insert(results, Raycast({x=node_pos.x+0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
- else
- table.insert(results, Raycast({x=node_pos.x-0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
- end
- if player_pos.y > node_pos.y then
- table.insert(results, Raycast({y=node_pos.y+0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
- else
- table.insert(results, Raycast({y=node_pos.y-0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
- end
- if player_pos.z > node_pos.z then
- table.insert(results, Raycast({z=node_pos.z+0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
- else
- table.insert(results, Raycast({z=node_pos.z-0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
- end
- return results
- end
- attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
- -- First check a simple degenerate case; if there are no special modifier nodes and the default attenuation
- -- is 1 then we don't need to bother with any detailed checking, the damage goes through unmodified.
- if default_attenuation == 1 and attenuation_nodes == nil and attenuation_groups == nil then return 1 end
- local raycasts = get_raycasts(node_pos, player_pos)
- local farthest_from_zero = 0
- for _, raycast in pairs(raycasts) do
- local current_attenuation = 1
- for ray_node in raycast do
- local ray_node_name = minetest.get_node(ray_node.under).name
- local ray_node_val = get_val(ray_node_name, attenuation_nodes, attenuation_groups)
- if ray_node_val == nil then ray_node_val = default_attenuation end
- current_attenuation = current_attenuation * ray_node_val
- if current_attenuation == 0 then break end -- once we hit zero no further checks are needed, it will never change.
- end
- -- By always selecting the farthest value from zero we accomodate both "healing" and "harmful" radiation
- -- and always let the most impactful value of either type through.
- -- If you've got both positive and negative modifiers (for example, if you've got a magical node that turns
- -- harmful radiation into healing radiation when it passes through) this could result in somewhat erratic effects.
- -- But that's part of the fun, eh? Players will just need to design and use their healing ray carefully.
- if math.abs(current_attenuation) > math.abs(farthest_from_zero) then
- farthest_from_zero = current_attenuation
- end
- end
- return farthest_from_zero
- end
- else
- -- Pre-Minetest 0.5 version. Attenuation_nodes and attenuation_groups are ignored
- attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
- if default_attenuation == 1 then return 1 end -- if default_attenuation is 1, don't attenuate.
- -- otherwise, it's all-or-nothing:
- if player_pos.y > node_pos.y then
- 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
- else
- 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
- end
- if player_pos.x > node_pos.x then
- 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
- else
- 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
- end
- if player_pos.z > node_pos.z then
- 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
- else
- 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
- end
- return 0
- end
- end
- radiant_damage.registered_damage_types = {}
- -- This method will update the registered_damage_types table with new values, keeping all the parameters
- -- consistent and in proper relation to each other.
- local update_damage_type = function(damage_name, new_def)
- if radiant_damage.registered_damage_types[damage_name] == nil then
- radiant_damage.registered_damage_types[damage_name] = {}
- end
- local damage_def = radiant_damage.registered_damage_types[damage_name]
- -- Interval
- if new_def.interval ~= nil then
- damage_def.interval = new_def.interval
- elseif damage_def.interval == nil then
- damage_def.interval = 1
- end
- -- Inverse square falloff
- if new_def.inverse_square_falloff ~= nil then
- damage_def.inverse_square_falloff = new_def.inverse_square_falloff
- elseif damage_def.inverse_square_falloff == nil then
- damage_def.inverse_square_falloff = true
- end
- -- Default attenuation value
- if new_def.default_attenuation ~= nil then
- damage_def.default_attenuation = new_def.default_attenuation
- elseif damage_def.default_attenuation == nil then
- damage_def.default_attenuation = 0
- end
- -- Above Only
- damage_def.above_only = new_def.above_only -- default to false
- -- on_damage callback
- damage_def.on_damage = new_def.on_damage
- -- it is efficient to split the emission and attenuation data into separate node and group maps.
- -- Emitted by
- damage_def.emission_nodes = damage_def.emission_nodes or {}
- damage_def.emission_groups = damage_def.emission_groups or {}
- for nodename, damage in pairs(new_def.emitted_by) do
- if damage == 0 then damage = nil end -- causes removal of damage-0 node types
- if string.sub(nodename, 1, 6) == "group:" then
- damage_def.emission_groups[string.sub(nodename, 7)] = damage -- omit the "group:" prefix
- else
- damage_def.emission_nodes[nodename] = damage
- end
- end
- damage_def.nodenames = damage_def.nodenames or {} -- for use with minetest.find_nodes_in_area
- for nodename, damage in pairs(new_def.emitted_by) do
- local handled = false
- for i, v in ipairs(damage_def.nodenames) do
- if v == nodename then
- if damage == 0 then
- table.remove(damage_def.nodenames, i)
- end
- handled = true
- break
- end
- end
- if not handled then
- table.insert(damage_def.nodenames, nodename)
- end
- end
- -- These remain nil unless some valid data is provided.
- if new_def.attenuated_by and Raycast then
- for nodename, attenuation in pairs(new_def.attenuated_by) do
- damage_def.attenuation_nodes = damage_def.attenuation_nodes or {}
- damage_def.attenuation_groups = damage_def.attenuation_groups or {}
- if string.sub(nodename, 1, 6) == "group:" then
- damage_def.attenuation_groups[string.sub(nodename, 7)] = attenuation -- omit the "group:" prefix
- else
- damage_def.attenuation_nodes[nodename] = attenuation
- end
- end
- end
- -- remove any attenuation nodes or groups that match the default attenuation, they're pointless.
- if damage_def.attenuation_groups then
- for node, attenuation in pairs(damage_def.attenuation_groups) do
- if attenuation == damage_def.default_attenuation then
- damage_def.attenuation_groups[node] = nil
- end
- end
- end
- if damage_def.attenuation_nodes then
- for node, attenuation in pairs(damage_def.attenuation_nodes) do
- if attenuation == damage_def.default_attenuation then
- damage_def.attenuation_nodes[node] = nil
- end
- end
- end
- -- Range
- if new_def.range ~= nil then
- damage_def.absolute_range = true
- damage_def.range = new_def.range
- elseif damage_def.inverse_square_falloff and not damage_def.absolute_range then
- damage_def.range = 0
- for _, damage in pairs(damage_def.emission_nodes) do
- damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
- end
- for _, damage in pairs(damage_def.emission_groups) do
- damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
- end
- end
- return damage_def
- end
- radiant_damage.override_radiant_damage = function(damage_name, damage_def)
- if radiant_damage.registered_damage_types[damage_name] then
- update_damage_type(damage_name, damage_def)
- elseif minetest.settings:get_bool("enable_damage") then
- minetest.log("error", "Attempt was made to override unregistered radiant_damage type " .. damage_name)
- end
- end
- radiant_damage.register_radiant_damage = function(damage_name, damage_def)
- if not minetest.settings:get_bool("enable_damage") then return end -- don't bother if enable_damage isn't set.
- if radiant_damage.registered_damage_types[damage_name] then
- minetest.log("error", "Attempt was made to register the already-registered radiant_damage type " .. damage_name)
- return
- end
- local damage_def = update_damage_type(damage_name, damage_def)
- local range = damage_def.range
- local above_only = damage_def.above_only
- local nodenames = damage_def.nodenames
- local default_attenuation = damage_def.default_attenuation
- local attenuation_nodes = damage_def.attenuation_nodes
- local attenuation_groups = damage_def.attenuation_groups
- local emission_nodes = damage_def.emission_nodes
- local emission_groups = damage_def.emission_groups
- local inverse_square_falloff = damage_def.inverse_square_falloff
- local on_damage = damage_def.on_damage
- local interval = damage_def.interval
- local timer = 0
- minetest.register_globalstep(function(dtime)
- timer = timer + dtime
- if timer >= interval then
- timer = timer - interval
- for _, player in pairs(minetest.get_connected_players()) do
- 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
- player_pos.y = player_pos.y + 1
- local rounded_pos = vector.round(player_pos)
- local nearby_nodes
- if above_only then
- nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, {x=0, y= -range, z=0}), rounded_pos, nodenames)
- else
- nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, -range), vector.add(rounded_pos, range), nodenames)
- end
- local total_damage = 0
- for _, node_pos in ipairs(nearby_nodes) do
- local distance
- if above_only then
- distance = math.max(player_pos.y - node_pos.y, 1)
- else
- 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
- end
- if distance <= range then
- local attenuation = attenuation_check(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
- if attenuation ~= 0 then
- local damage = get_val(minetest.get_node(node_pos).name, emission_nodes, emission_groups)
- if inverse_square_falloff then
- total_damage = total_damage + (damage / (distance * distance)) * attenuation
- else
- total_damage = total_damage + damage * attenuation
- end
- end
- end
- end
- if on_damage == nil then
- total_damage = math.floor(total_damage)
- if total_damage ~= 0 then
- minetest.log("action", player:get_player_name() .. " takes " .. tostring(total_damage) .. " damage from " .. damage_name .. " radiant damage at " .. minetest.pos_to_string(rounded_pos))
- player:set_hp(player:get_hp() - total_damage)
- end
- else
- on_damage(player, total_damage, rounded_pos)
- end
- end
- end
- end)
- end
- if radiant_damage.config.enable_heat_damage then
- local on_fire_damage
- if minetest.get_modpath("3d_armor") and armor ~= nil then
- -- 3d_armor uses a strange fire protection system different from all the rest, wherein
- -- its armor protects wholly against some heat sources and not at all against others
- -- based on how "hot" they are and how strong against fire the armor is.
- -- Level 1 protects against a wall torch, level 3 protects against a basic fire, and level 5 protects against lava.
- -- Converting this into a standard armor type is going to require some arbitrary decisions.
- -- My decision is: level 5 protection should reduce the default damage from lava immersion from 8 to 0.5 hp (0.0625 multiplier).
- -- Level 3 protection reduces the default damage from basic flame from 4 to 0.5 hp (0.125 multiplier)
- -- Torches don't do damage in default so I will ignore that.
- -- Level 0 has a damage multiplier of 1.
- -- That gives us three data points: (0,1), (3,0.125), (5,0.0625). Fitting that to an exponential curve gives us:
- -- y = 0.0481417 + 0.9518583*e^(-0.8388176*x)
- -- Which looks about right on a graph, and "looks about right on a graph" is good enough for me.
- on_fire_damage = function(player, damage, pos)
- local fire_protection = armor.def[player:get_player_name()].fire
- local fire_multiplier = 1
- if fire_protection then
- fire_multiplier = math.min(1, 0.0481417 + 0.9518583 * math.exp(-0.8388176*fire_protection))
- end
- -- If the player also has conventional fire armor, use whatever's better.
- local fire_armor = player:get_armor_groups().fire
- if fire_armor then
- fire_multiplier = math.min(fire_multiplier, fire_armor/100)
- end
- damage = math.floor(damage * fire_multiplier)
- if damage > 0 then
- minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
- player:set_hp(player:get_hp() - damage)
- minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
- end
- end
- else
- on_fire_damage = function(player, damage, pos)
- local fire_armor = player:get_armor_groups().fire
- if fire_armor then
- damage = damage * fire_armor / 100
- end
- damage = math.floor(damage)
- if damage > 0 then
- minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
- player:set_hp(player:get_hp() - damage)
- minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
- end
- end
- end
- radiant_damage.register_radiant_damage("heat", {
- interval = 1,
- emitted_by = {["group:lava"] = radiant_damage.config.lava_damage, ["fire:basic_flame"] = radiant_damage.config.fire_damage,
- ["fire:permanent_flame"] = radiant_damage.config.fire_damage, ['nether:lava_crust'] = .25,},
- inverse_square_falloff = true,
- default_attenuation = 0, -- heat is blocked by anything.
- on_damage = on_fire_damage,
- })
- end
- if radiant_damage.config.enable_mese_damage then
- local shields = {"default:steelblock", "default:copperblock", "default:tinblock", "default:bronzeblock", "default:goldblock"}
- local amplifiers = {"default:diamondblock", "default:coalblock"}
- for _, shielding_node in ipairs(shields) do
- local node_def = minetest.registered_nodes[shielding_node]
- if node_def then
- local new_groups = node_def.groups or {}
- new_groups.mese_radiation_shield = 1
- minetest.override_item(shielding_node, {groups=new_groups})
- end
- end
- for _, amp_node in ipairs(amplifiers) do
- local node_def = minetest.registered_nodes[amp_node]
- if node_def then
- local new_groups = node_def.groups or {}
- new_groups.mese_radiation_amplifier = 1
- minetest.override_item(amp_node, {groups=new_groups})
- end
- end
- local on_radiation_damage = function(player, damage, pos)
- local radiation_multiplier = player:get_armor_groups().radiation
- if radiation_multiplier then
- damage = damage * radiation_multiplier / 100
- end
- damage = math.floor(damage)
- if damage > 0 then
- minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from mese radiation damage at " .. minetest.pos_to_string(pos))
- player:set_hp(player:get_hp() - damage)
- minetest.sound_play({name = "radiant_damage_geiger", gain = math.min(1, damage/10)}, {to_player=player:get_player_name()})
- end
- end
- radiant_damage.register_radiant_damage("mese", {
- interval = radiant_damage.config.mese_interval,
- inverse_square_falloff = true,
- emitted_by = {["default:stone_with_mese"] = radiant_damage.config.mese_damage, ["default:mese"] = radiant_damage.config.mese_damage * 9},
- attenuated_by = {["group:stone"] = 0.5, ["group:mese_radiation_shield"] = 0.1, ["group:mese_radiation_amplifier"] = 4},
- default_attenuation = 0.9,
- on_damage = on_radiation_damage,
- })
- end
|