442 lines
18 KiB
Lua
442 lines
18 KiB
Lua
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
|