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