Vanessa Ezekowitz
2017-03-23 cb5e3e8ff4ffb70e9513ebb005f2a825d4e16504
commit | author | age
1da213 1 --[[
S 2 Radioactivity
3
4 Radiation resistance represents the extent to which a material
5 attenuates radiation passing through it; i.e., how good a radiation
6 shield it is.  This is identified per node type.  For materials that
7 exist in real life, the radiation resistance value that this system
8 uses for a node type consisting of a solid cube of that material is the
9 (approximate) number of halvings of ionising radiation that is achieved
10 by a meter of the material in real life.  This is approximately
11 proportional to density, which provides a good way to estimate it.
12 Homogeneous mixtures of materials have radiation resistance computed
13 by a simple weighted mean.  Note that the amount of attenuation that
14 a material achieves in-game is not required to be (and is not) the
15 same as the attenuation achieved in real life.
16
17 Radiation resistance for a node type may be specified in the node
18 definition, under the key "radiation_resistance".  As an interim
19 measure, until node definitions widely include this, this code
20 knows a bunch of values for particular node types in several mods,
21 and values for groups of node types.  The node definition takes
22 precedence if it specifies a value.  Nodes for which no value at
23 all is known are taken to provide no radiation resistance at all;
24 this is appropriate for the majority of node types.  Only node types
25 consisting of a fairly homogeneous mass of material should report
26 non-zero radiation resistance; anything with non-uniform geometry
27 or complex internal structure should show no radiation resistance.
28 Fractional resistance values are permitted.
29 --]]
30
31 local S = technic.getter
32
33 local rad_resistance_node = {
34     ["default:brick"] = 13,
35     ["default:bronzeblock"] = 45,
36     ["default:clay"] = 15,
37     ["default:coalblock"] = 9.6,
38     ["default:cobble"] = 15,
39     ["default:copperblock"] = 46,
40     ["default:desert_cobble"] = 15,
41     ["default:desert_sand"] = 10,
42     ["default:desert_stone"] = 17,
43     ["default:desert_stonebrick"] = 17,
44     ["default:diamondblock"] = 24,
45     ["default:dirt"] = 8.2,
46     ["default:dirt_with_grass"] = 8.2,
47     ["default:dirt_with_grass_footsteps"] = 8.2,
48     ["default:dirt_with_snow"] = 8.2,
49     ["default:glass"] = 17,
50     ["default:goldblock"] = 170,
51     ["default:gravel"] = 10,
52     ["default:ice"] = 5.6,
53     ["default:lava_flowing"] = 8.5,
54     ["default:lava_source"] = 17,
55     ["default:mese"] = 21,
56     ["default:mossycobble"] = 15,
cb5e3e 57     ["default:nyancat"] = 10000,
VE 58     ["default:nyancat_rainbow"] = 10000,
59     ["nyancat:nyancat"] = 10000,
60     ["nyancat:nyancat_rainbow"] = 10000,
1da213 61     ["default:obsidian"] = 18,
S 62     ["default:obsidian_glass"] = 18,
63     ["default:sand"] = 10,
64     ["default:sandstone"] = 15,
65     ["default:sandstonebrick"] = 15,
66     ["default:snowblock"] = 1.7,
67     ["default:steelblock"] = 40,
68     ["default:stone"] = 17,
69     ["default:stone_with_coal"] = 16,
70     ["default:stone_with_copper"] = 20,
71     ["default:stone_with_diamond"] = 18,
72     ["default:stone_with_gold"] = 34,
73     ["default:stone_with_iron"] = 20,
74     ["default:stone_with_mese"] = 17,
75     ["default:stonebrick"] = 17,
76     ["default:water_flowing"] = 2.8,
77     ["default:water_source"] = 5.6,
78     ["farming:desert_sand_soil"] = 10,
79     ["farming:desert_sand_soil_wet"] = 10,
80     ["farming:soil"] = 8.2,
81     ["farming:soil_wet"] = 8.2,
82     ["glooptest:akalin_crystal_glass"] = 21,
83     ["glooptest:akalinblock"] = 40,
84     ["glooptest:alatro_crystal_glass"] = 21,
85     ["glooptest:alatroblock"] = 40,
86     ["glooptest:amethystblock"] = 18,
87     ["glooptest:arol_crystal_glass"] = 21,
88     ["glooptest:crystal_glass"] = 21,
89     ["glooptest:emeraldblock"] = 19,
90     ["glooptest:heavy_crystal_glass"] = 21,
91     ["glooptest:mineral_akalin"] = 20,
92     ["glooptest:mineral_alatro"] = 20,
93     ["glooptest:mineral_amethyst"] = 17,
94     ["glooptest:mineral_arol"] = 20,
95     ["glooptest:mineral_desert_coal"] = 16,
96     ["glooptest:mineral_desert_iron"] = 20,
97     ["glooptest:mineral_emerald"] = 17,
98     ["glooptest:mineral_kalite"] = 20,
99     ["glooptest:mineral_ruby"] = 18,
100     ["glooptest:mineral_sapphire"] = 18,
101     ["glooptest:mineral_talinite"] = 20,
102     ["glooptest:mineral_topaz"] = 18,
103     ["glooptest:reinforced_crystal_glass"] = 21,
104     ["glooptest:rubyblock"] = 27,
105     ["glooptest:sapphireblock"] = 27,
106     ["glooptest:talinite_crystal_glass"] = 21,
107     ["glooptest:taliniteblock"] = 40,
108     ["glooptest:topazblock"] = 24,
109     ["mesecons_extrawires:mese_powered"] = 21,
110     ["moreblocks:cactus_brick"] = 13,
111     ["moreblocks:cactus_checker"] = 8.5,
112     ["moreblocks:circle_stone_bricks"] = 17,
113     ["moreblocks:clean_glass"] = 17,
114     ["moreblocks:coal_checker"] = 9.0,
115     ["moreblocks:coal_glass"] = 17,
116     ["moreblocks:coal_stone"] = 17,
117     ["moreblocks:coal_stone_bricks"] = 17,
118     ["moreblocks:glow_glass"] = 17,
119     ["moreblocks:grey_bricks"] = 15,
120     ["moreblocks:iron_checker"] = 11,
121     ["moreblocks:iron_glass"] = 17,
122     ["moreblocks:iron_stone"] = 17,
123     ["moreblocks:iron_stone_bricks"] = 17,
124     ["moreblocks:plankstone"] = 9.3,
125     ["moreblocks:split_stone_tile"] = 15,
126     ["moreblocks:split_stone_tile_alt"] = 15,
127     ["moreblocks:stone_tile"] = 15,
128     ["moreblocks:super_glow_glass"] = 17,
129     ["moreblocks:tar"] = 7.0,
130     ["moreblocks:wood_tile"] = 1.7,
131     ["moreblocks:wood_tile_center"] = 1.7,
132     ["moreblocks:wood_tile_down"] = 1.7,
133     ["moreblocks:wood_tile_flipped"] = 1.7,
134     ["moreblocks:wood_tile_full"] = 1.7,
135     ["moreblocks:wood_tile_left"] = 1.7,
136     ["moreblocks:wood_tile_right"] = 1.7,
137     ["moreblocks:wood_tile_up"] = 1.7,
138     ["moreores:mineral_mithril"] = 18,
139     ["moreores:mineral_silver"] = 21,
140     ["moreores:mineral_tin"] = 19,
141     ["moreores:mithril_block"] = 26,
142     ["moreores:silver_block"] = 53,
143     ["moreores:tin_block"] = 37,
144     ["snow:snow_brick"] = 2.8,
145     ["technic:brass_block"] = 43,
146     ["technic:carbon_steel_block"] = 40,
147     ["technic:cast_iron_block"] = 40,
148     ["technic:chernobylite_block"] = 40,
149     ["technic:chromium_block"] = 37,
150     ["technic:corium_flowing"] = 40,
151     ["technic:corium_source"] = 80,
152     ["technic:granite"] = 18,
153     ["technic:lead_block"] = 80,
154     ["technic:marble"] = 18,
155     ["technic:marble_bricks"] = 18,
156     ["technic:mineral_chromium"] = 19,
157     ["technic:mineral_uranium"] = 71,
158     ["technic:mineral_zinc"] = 19,
159     ["technic:stainless_steel_block"] = 40,
160     ["technic:zinc_block"] = 36,
161     ["tnt:tnt"] = 11,
162     ["tnt:tnt_burning"] = 11,
163 }
164 local rad_resistance_group = {
165     concrete = 16,
166     tree = 3.4,
167     uranium_block = 500,
168     wood = 1.7,
169 }
170 local cache_radiation_resistance = {}
171 local function node_radiation_resistance(node_name)
172     local resistance = cache_radiation_resistance[node_name]
173     if resistance then
174         return resistance
175     end
176     local def = minetest.registered_nodes[node_name]
177     if not def then
178         cache_radiation_resistance[node_name] = 0
179         return 0
180     end
181     resistance = def.radiation_resistance or
182             rad_resistance_node[node_name]
183     if not resistance then
184         resistance = 0
185         for g, v in pairs(def.groups) do
186             if v > 0 and rad_resistance_group[g] then
187                 resistance = resistance + rad_resistance_group[g]
188             end
189         end
190     end
191     resistance = math.sqrt(resistance)
192     cache_radiation_resistance[node_name] = resistance
193     return resistance
194 end
195
196
197 --[[
198 Radioactive nodes cause damage to nearby players.  The damage
199 effect depends on the intrinsic strength of the radiation source,
200 the distance between the source and the player, and the shielding
201 effect of the intervening material.  These determine a rate of damage;
202 total damage caused is the integral of this over time.
203
204 In the absence of effective shielding, for a specific source the
205 damage rate varies realistically in inverse proportion to the square
206 of the distance.  (Distance is measured to the player's abdomen,
207 not to the nominal player position which corresponds to the foot.)
208 However, if the player is inside a non-walkable (liquid or gaseous)
209 radioactive node, the nominal distance could go to zero, yielding
210 infinite damage.  In that case, the player's body is displacing the
211 radioactive material, so the effective distance should remain non-zero.
212 We therefore apply a lower distance bound of sqrt(0.75), which is
213 the maximum distance one can get from the node center within the node.
214
215 A radioactive node is identified by being in the "radioactive" group,
216 and the group value signifies the strength of the radiation source.
217 The group value is the distance from a node at which an unshielded
218 player will be damaged by 1 HP/s.  Or, equivalently, it is the square
219 root of the damage rate in HP/s that an unshielded player one node
220 away will take.
221
222 Shielding is assessed by adding the shielding values of all nodes
223 between the source node and the player, ignoring the source node itself.
224 As in reality, shielding causes exponential attenuation of radiation.
225 However, the effect is scaled down relative to real life.  A node with
226 radiation resistance value R yields attenuation of sqrt(R) * 0.1 nepers.
227 (In real life it would be about R * 0.69 nepers, by the definition
228 of the radiation resistance values.)  The sqrt part of this formula
229 scales down the differences between shielding types, reflecting the
230 game's simplification of making expensive materials such as gold
231 readily available in cubes.  The multiplicative factor in the
232 formula scales down the difference between shielded and unshielded
233 safe distances, avoiding the latter becoming impractically large.
234
06dec2 235 Damage is processed at rates down to 0.2 HP/s, which in the absence of
1da213 236 shielding is attained at the distance specified by the "radioactive"
06dec2 237 group value.  Computed damage rates below 0.2 HP/s result in no
1da213 238 damage at all to the player.  This gives the player an opportunity
S 239 to be safe, and limits the range at which source/player interactions
240 need to be considered.
241 --]]
242 local abdomen_offset = 1
243 local cache_scaled_shielding = {}
06dec2 244 local rad_dmg_cutoff = 0.2
S 245 local radiated_players = {}
1da213 246
cbe974 247 local armor_enabled = technic.config:get_bool("enable_radiation_protection")
b739ed 248 local entity_damage = technic.config:get_bool("enable_entity_radiation_damage")
NZ 249 local longterm_damage = technic.config:get_bool("enable_longterm_radiation_damage")
250
06dec2 251 local function apply_fractional_damage(o, dmg)
1da213 252     local dmg_int = math.floor(dmg)
S 253     -- The closer you are to getting one more damage point,
254     -- the more likely it will be added.
255     if math.random() < dmg - dmg_int then
256         dmg_int = dmg_int + 1
257     end
258     if dmg_int > 0 then
06dec2 259         local new_hp = math.max(o:get_hp() - dmg_int, 0)
S 260         o:set_hp(new_hp)
261         return new_hp == 0
1da213 262     end
06dec2 263     return false
S 264 end
265
1810f4 266 local function calculate_base_damage(node_pos, object_pos, strength)
06dec2 267     local shielding = 0
1810f4 268     local dist = vector.distance(node_pos, object_pos)
06dec2 269
1810f4 270     for ray_pos in technic.trace_node_ray(node_pos,
NZ 271             vector.direction(node_pos, object_pos), dist) do
06dec2 272         local shield_name = minetest.get_node(ray_pos).name
51be33 273         shielding = shielding + node_radiation_resistance(shield_name) * 0.025
06dec2 274     end
S 275
276     local dmg = (strength * strength) /
277         (math.max(0.75, dist * dist) * math.exp(shielding))
278
279     if dmg < rad_dmg_cutoff then return end
1810f4 280     return dmg
NZ 281 end
06dec2 282
1810f4 283 local function calculate_damage_multiplier(object)
NZ 284     local ag = object.get_armor_groups and object:get_armor_groups()
285     if not ag then
286         return 0
287     end
73afc4 288     if ag.immortal then
NZ 289         return 0
290     end
1810f4 291     if ag.radiation then
NZ 292         return 0.01 * ag.radiation
293     end
294     if ag.fleshy then
295         return math.sqrt(0.01 * ag.fleshy)
296     end
297     return 0
298 end
299
300 local function calculate_object_center(object)
301     if object:is_player() then
302         return {x=0, y=abdomen_offset, z=0}
303     end
304     return {x=0, y=0, z=0}
305 end
306
307 local function dmg_object(pos, object, strength)
308     local obj_pos = vector.add(object:getpos(), calculate_object_center(object))
cbe974 309     local mul
NZ 310     if armor_enabled or entity_damage then
311         -- we need to check may the object be damaged even if armor is disabled
312         mul = calculate_damage_multiplier(object)
313         if mul == 0 then
314             return
315         end
1810f4 316     end
b739ed 317     local dmg = calculate_base_damage(pos, obj_pos, strength)
NZ 318     if not dmg then
319         return
320     end
cbe974 321     if armor_enabled then
NZ 322         dmg = dmg * mul
323     end
1810f4 324     apply_fractional_damage(object, dmg)
b739ed 325     if longterm_damage and object:is_player() then
1810f4 326         local pn = object:get_player_name()
NZ 327         radiated_players[pn] = (radiated_players[pn] or 0) + dmg
328     end
1da213 329 end
S 330
331 local rad_dmg_mult_sqrt = math.sqrt(1 / rad_dmg_cutoff)
332 local function dmg_abm(pos, node)
333     local strength = minetest.get_item_group(node.name, "radioactive")
334     local max_dist = strength * rad_dmg_mult_sqrt
335     for _, o in pairs(minetest.get_objects_inside_radius(pos,
336             max_dist + abdomen_offset)) do
b739ed 337         if entity_damage or o:is_player() then
NZ 338             dmg_object(pos, o, strength)
339         end
1da213 340     end
S 341 end
342
343 if minetest.setting_getbool("enable_damage") then
344     minetest.register_abm({
345         nodenames = {"group:radioactive"},
346         interval = 1,
347         chance = 1,
348         action = dmg_abm,
349     })
06dec2 350
b739ed 351     if longterm_damage then
NZ 352         minetest.register_globalstep(function(dtime)
353             for pn, dmg in pairs(radiated_players) do
354                 dmg = dmg - (dtime / 8)
355                 local player = minetest.get_player_by_name(pn)
356                 local killed
357                 if player and dmg > rad_dmg_cutoff then
358                     killed = apply_fractional_damage(player, (dmg * dtime) / 8)
359                 else
360                     dmg = nil
361                 end
362                 -- on_dieplayer will have already set this if the player died
363                 if not killed then
364                     radiated_players[pn] = dmg
365                 end
06dec2 366             end
b739ed 367         end)
06dec2 368
b739ed 369         minetest.register_on_dieplayer(function(player)
NZ 370             radiated_players[player:get_player_name()] = nil
371         end)
372     end
1da213 373 end
S 374
375 -- Radioactive materials that can result from destroying a reactor
376 local griefing = technic.config:get_bool("enable_corium_griefing")
377
378 for _, state in pairs({"flowing", "source"}) do
379     minetest.register_node("technic:corium_"..state, {
380         description = S(state == "source" and "Corium Source" or "Flowing Corium"),
381         drawtype = (state == "source" and "liquid" or "flowingliquid"),
382         [state == "source" and "tiles" or "special_tiles"] = {{
383             name = "technic_corium_"..state.."_animated.png",
384             animation = {
385                 type = "vertical_frames",
386                 aspect_w = 16,
387                 aspect_h = 16,
388                 length = 3.0,
389             },
390         }},
391         paramtype = "light",
392         paramtype2 = (state == "flowing" and "flowingliquid" or nil),
393         light_source = (state == "source" and 8 or 5),
394         walkable = false,
395         pointable = false,
396         diggable = false,
397         buildable_to = true,
398         drop = "",
399         drowning = 1,
400         liquidtype = state,
401         liquid_alternative_flowing = "technic:corium_flowing",
402         liquid_alternative_source = "technic:corium_source",
403         liquid_viscosity = LAVA_VISC,
404         liquid_renewable = false,
405         damage_per_second = 6,
406         post_effect_color = {a=192, r=80, g=160, b=80},
407         groups = {
408             liquid = 2,
409             hot = 3,
410             igniter = (griefing and 1 or 0),
06dec2 411             radioactive = (state == "source" and 12 or 6),
1da213 412             not_in_creative_inventory = (state == "flowing" and 1 or nil),
S 413         },
414     })
415 end
416
417 if rawget(_G, "bucket") and bucket.register_liquid then
418     bucket.register_liquid(
419         "technic:corium_source",
420         "technic:corium_flowing",
421         "technic:bucket_corium",
422         "technic_bucket_corium.png",
423         "Corium Bucket"
424     )
425 end
426
427 minetest.register_node("technic:chernobylite_block", {
428         description = S("Chernobylite Block"),
429     tiles = {"technic_chernobylite_block.png"},
430     is_ground_content = true,
06dec2 431     groups = {cracky=1, radioactive=4, level=2},
1da213 432     sounds = default.node_sound_stone_defaults(),
S 433     light_source = 2,
434 })
435
436 minetest.register_abm({
437     nodenames = {"group:water"},
438     neighbors = {"technic:corium_source"},
439     interval = 1,
440     chance = 1,
441     action = function(pos, node)
442         minetest.remove_node(pos)
443     end,
444 })
445
446 minetest.register_abm({
447     nodenames = {"technic:corium_flowing"},
448     neighbors = {"group:water"},
449     interval = 1,
450     chance = 1,
451     action = function(pos, node)
452         minetest.set_node(pos, {name="technic:chernobylite_block"})
453     end,
454 })
455
456 minetest.register_abm({
457     nodenames = {"technic:corium_flowing"},
458     interval = 5,
459     chance = (griefing and 10 or 1),
460     action = function(pos, node)
461         minetest.set_node(pos, {name="technic:chernobylite_block"})
462     end,
463 })
464
465 if griefing then
466     minetest.register_abm({
467         nodenames = {"technic:corium_source", "technic:corium_flowing"},
468         interval = 4,
469         chance = 4,
470         action = function(pos, node)
471             for _, offset in ipairs({
472                 vector.new(1,0,0),
473                 vector.new(-1,0,0),
474                 vector.new(0,0,1),
475                 vector.new(0,0,-1),
476                 vector.new(0,-1,0),
477             }) do
478                 if math.random(8) == 1 then
479                     minetest.dig_node(vector.add(pos, offset))
480                 end
481             end
482         end,
483     })
484 end
485