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