Epic/mods/radiant_damage/init.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