Christopher Head
2019-01-26 4f78a69ffc714886c9d6e812f78d543bb33fe674
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
297     end
298     if ag.fleshy then
299         return math.sqrt(0.01 * ag.fleshy)
300     end
301     return 0
302 end
303
304 local function calculate_object_center(object)
305     if object:is_player() then
306         return {x=0, y=abdomen_offset, z=0}
307     end
308     return {x=0, y=0, z=0}
309 end
310
311 local function dmg_object(pos, object, strength)
312     local obj_pos = vector.add(object:getpos(), calculate_object_center(object))
cbe974 313     local mul
NZ 314     if armor_enabled or entity_damage then
315         -- we need to check may the object be damaged even if armor is disabled
316         mul = calculate_damage_multiplier(object)
317         if mul == 0 then
318             return
319         end
1810f4 320     end
b739ed 321     local dmg = calculate_base_damage(pos, obj_pos, strength)
NZ 322     if not dmg then
323         return
324     end
cbe974 325     if armor_enabled then
NZ 326         dmg = dmg * mul
327     end
1810f4 328     apply_fractional_damage(object, dmg)
b739ed 329     if longterm_damage and object:is_player() then
1810f4 330         local pn = object:get_player_name()
NZ 331         radiated_players[pn] = (radiated_players[pn] or 0) + dmg
332     end
1da213 333 end
S 334
335 local rad_dmg_mult_sqrt = math.sqrt(1 / rad_dmg_cutoff)
336 local function dmg_abm(pos, node)
337     local strength = minetest.get_item_group(node.name, "radioactive")
338     local max_dist = strength * rad_dmg_mult_sqrt
339     for _, o in pairs(minetest.get_objects_inside_radius(pos,
340             max_dist + abdomen_offset)) do
b739ed 341         if entity_damage or o:is_player() then
NZ 342             dmg_object(pos, o, strength)
343         end
1da213 344     end
S 345 end
346
97e1c8 347 if minetest.settings:get_bool("enable_damage") then
1da213 348     minetest.register_abm({
78f16c 349         label = "Radiation damage",
1da213 350         nodenames = {"group:radioactive"},
S 351         interval = 1,
352         chance = 1,
353         action = dmg_abm,
354     })
06dec2 355
b739ed 356     if longterm_damage then
NZ 357         minetest.register_globalstep(function(dtime)
358             for pn, dmg in pairs(radiated_players) do
359                 dmg = dmg - (dtime / 8)
360                 local player = minetest.get_player_by_name(pn)
361                 local killed
362                 if player and dmg > rad_dmg_cutoff then
363                     killed = apply_fractional_damage(player, (dmg * dtime) / 8)
364                 else
365                     dmg = nil
366                 end
367                 -- on_dieplayer will have already set this if the player died
368                 if not killed then
369                     radiated_players[pn] = dmg
370                 end
06dec2 371             end
b739ed 372         end)
06dec2 373
b739ed 374         minetest.register_on_dieplayer(function(player)
NZ 375             radiated_players[player:get_player_name()] = nil
376         end)
377     end
1da213 378 end
S 379
380 -- Radioactive materials that can result from destroying a reactor
381 local griefing = technic.config:get_bool("enable_corium_griefing")
382
383 for _, state in pairs({"flowing", "source"}) do
384     minetest.register_node("technic:corium_"..state, {
385         description = S(state == "source" and "Corium Source" or "Flowing Corium"),
386         drawtype = (state == "source" and "liquid" or "flowingliquid"),
83a4bb 387         tiles = {{
1da213 388             name = "technic_corium_"..state.."_animated.png",
S 389             animation = {
390                 type = "vertical_frames",
391                 aspect_w = 16,
392                 aspect_h = 16,
393                 length = 3.0,
394             },
395         }},
83a4bb 396         special_tiles = {
N 397             {
398                 name = "technic_corium_"..state.."_animated.png",
399                 backface_culling = false,
400                 animation = {
401                     type = "vertical_frames",
402                     aspect_w = 16,
403                     aspect_h = 16,
404                     length = 3.0,
405                 },
406             },
407             {
408                 name = "technic_corium_"..state.."_animated.png",
409                 backface_culling = true,
410                 animation = {
411                     type = "vertical_frames",
412                     aspect_w = 16,
413                     aspect_h = 16,
414                     length = 3.0,
415                 },
416             },
417         },
1da213 418         paramtype = "light",
S 419         paramtype2 = (state == "flowing" and "flowingliquid" or nil),
420         light_source = (state == "source" and 8 or 5),
421         walkable = false,
422         pointable = false,
423         diggable = false,
424         buildable_to = true,
425         drop = "",
426         drowning = 1,
427         liquidtype = state,
428         liquid_alternative_flowing = "technic:corium_flowing",
429         liquid_alternative_source = "technic:corium_source",
430         liquid_viscosity = LAVA_VISC,
431         liquid_renewable = false,
432         damage_per_second = 6,
433         post_effect_color = {a=192, r=80, g=160, b=80},
434         groups = {
435             liquid = 2,
436             hot = 3,
437             igniter = (griefing and 1 or 0),
06dec2 438             radioactive = (state == "source" and 12 or 6),
1da213 439             not_in_creative_inventory = (state == "flowing" and 1 or nil),
S 440         },
441     })
442 end
443
444 if rawget(_G, "bucket") and bucket.register_liquid then
445     bucket.register_liquid(
446         "technic:corium_source",
447         "technic:corium_flowing",
448         "technic:bucket_corium",
449         "technic_bucket_corium.png",
450         "Corium Bucket"
451     )
452 end
453
454 minetest.register_node("technic:chernobylite_block", {
455         description = S("Chernobylite Block"),
456     tiles = {"technic_chernobylite_block.png"},
457     is_ground_content = true,
06dec2 458     groups = {cracky=1, radioactive=4, level=2},
1da213 459     sounds = default.node_sound_stone_defaults(),
S 460     light_source = 2,
461 })
462
463 minetest.register_abm({
78f16c 464     label = "Corium: boil-off water (sources)",
1da213 465     nodenames = {"group:water"},
S 466     neighbors = {"technic:corium_source"},
467     interval = 1,
468     chance = 1,
469     action = function(pos, node)
470         minetest.remove_node(pos)
471     end,
472 })
473
474 minetest.register_abm({
78f16c 475     label = "Corium: boil-off water (flowing)",
1da213 476     nodenames = {"technic:corium_flowing"},
S 477     neighbors = {"group:water"},
478     interval = 1,
479     chance = 1,
480     action = function(pos, node)
481         minetest.set_node(pos, {name="technic:chernobylite_block"})
482     end,
483 })
484
485 minetest.register_abm({
78f16c 486     label = "Corium: become chernobylite",
1da213 487     nodenames = {"technic:corium_flowing"},
S 488     interval = 5,
489     chance = (griefing and 10 or 1),
490     action = function(pos, node)
491         minetest.set_node(pos, {name="technic:chernobylite_block"})
492     end,
493 })
494
495 if griefing then
496     minetest.register_abm({
78f16c 497         label = "Corium: griefing",
1da213 498         nodenames = {"technic:corium_source", "technic:corium_flowing"},
S 499         interval = 4,
500         chance = 4,
501         action = function(pos, node)
502             for _, offset in ipairs({
503                 vector.new(1,0,0),
504                 vector.new(-1,0,0),
505                 vector.new(0,0,1),
506                 vector.new(0,0,-1),
507                 vector.new(0,-1,0),
508             }) do
509                 if math.random(8) == 1 then
510                     minetest.dig_node(vector.add(pos, offset))
511                 end
512             end
513         end,
514     })
515 end
516