Update falling nodes when fire replaces a block
[MineClone/MineClone2.git] / mods / ITEMS / mcl_fire / init.lua
blob381e47373973a54d84453a449f01b91b6239a8b0
1 -- Global namespace for functions
3 mcl_fire = {}
5 local S = minetest.get_translator("mcl_fire")
6 local N = function(s) return s end
8 --
9 -- Items
12 -- Flame nodes
14 -- Fire settings
16 -- When enabled, fire destroys other blocks.
17 local fire_enabled = minetest.settings:get_bool("enable_fire", true)
19 -- Enable sound
20 local flame_sound = minetest.settings:get_bool("flame_sound", true)
22 -- Help texts
23 local fire_help, eternal_fire_help
24 if fire_enabled then
25 fire_help = S("Fire is a damaging and destructive but short-lived kind of block. It will destroy and spread towards near flammable blocks, but fire will disappear when there is nothing to burn left. It will be extinguished by nearby water and rain. Fire can be destroyed safely by punching it, but it is hurtful if you stand directly in it. If a fire is started above netherrack or a magma block, it will immediately turn into an eternal fire.")
26 else
27 fire_help = S("Fire is a damaging but non-destructive short-lived kind of block. It will disappear when there is no flammable block around. Fire does not destroy blocks, at least not in this world. It will be extinguished by nearby water and rain. Fire can be destroyed safely by punching it, but it is hurtful if you stand directly in it. If a fire is started above netherrack or a magma block, it will immediately turn into an eternal fire.")
28 end
30 if fire_enabled then
31 eternal_fire_help = S("Eternal fire is a damaging block that might create more fire. It will create fire around it when flammable blocks are nearby. Eternal fire can be extinguished by punches and nearby water blocks. Other than (normal) fire, eternal fire does not get extinguished on its own and also continues to burn under rain. Punching eternal fire is safe, but it hurts if you stand inside.")
32 else
33 eternal_fire_help = S("Eternal fire is a damaging block. Eternal fire can be extinguished by punches and nearby water blocks. Other than (normal) fire, eternal fire does not get extinguished on its own and also continues to burn under rain. Punching eternal fire is safe, but it hurts if you stand inside.")
34 end
36 local fire_death_messages = {
37 N("@1 has been cooked crisp."),
38 N("@1 felt the burn."),
39 N("@1 died in the flames."),
40 N("@1 died in a fire."),
43 local fire_timer = function(pos)
44 minetest.get_node_timer(pos):start(math.random(3, 7))
45 end
47 local spawn_fire = function(pos, age)
48 minetest.set_node(pos, {name="mcl_fire:fire", param2 = age})
49 minetest.check_single_for_falling({x=pos.x, y=pos.y+1, z=pos.z})
50 end
52 minetest.register_node("mcl_fire:fire", {
53 description = S("Fire"),
54 _doc_items_longdesc = fire_help,
55 drawtype = "firelike",
56 tiles = {
58 name = "fire_basic_flame_animated.png",
59 animation = {
60 type = "vertical_frames",
61 aspect_w = 16,
62 aspect_h = 16,
63 length = 1
67 inventory_image = "fire_basic_flame.png",
68 paramtype = "light",
69 light_source = minetest.LIGHT_MAX,
70 walkable = false,
71 buildable_to = true,
72 sunlight_propagates = true,
73 damage_per_second = 1,
74 _mcl_node_death_message = fire_death_messages,
75 groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston=1, destroys_items=1 },
76 floodable = true,
77 on_flood = function(pos, oldnode, newnode)
78 if minetest.get_item_group(newnode.name, "water") ~= 0 then
79 minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
80 end
81 end,
82 on_timer = function(pos)
83 local node = minetest.get_node(pos)
84 -- Age is a number from 0 to 15 and is increased every timer step.
85 -- "old" fire is more likely to be extinguished
86 local age = node.param2
87 local flammables = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"group:flammable"})
88 local below = minetest.get_node({x=pos.x, y=pos.z-1, z=pos.z})
89 local below_is_flammable = minetest.get_item_group(below.name, "flammable") > 0
90 -- Extinguish fire
91 if (not fire_enabled) and (math.random(1,3) == 1) then
92 minetest.remove_node(pos)
93 return
94 end
95 if age == 15 and not below_is_flammable then
96 minetest.remove_node(pos)
97 return
98 elseif age > 3 and #flammables == 0 and not below_is_flammable and math.random(1,4) == 1 then
99 minetest.remove_node(pos)
100 return
102 local age_add = 1
103 -- If fire spread is disabled, we have to skip the "destructive" code
104 if (not fire_enabled) then
105 if age + age_add <= 15 then
106 node.param2 = age + age_add
107 minetest.set_node(pos, node)
109 -- Restart timer
110 fire_timer(pos)
111 return
113 -- Spawn fire to nearby flammable nodes
114 local is_next_to_flammable = minetest.find_node_near(pos, 2, {"group:flammable"}) ~= nil
115 if is_next_to_flammable and math.random(1,2) == 1 then
116 -- The fire we spawn copies the age of this fire.
117 -- This prevents fire from spreading infinitely far as the fire fire dies off
118 -- quicker the further it has spreaded.
119 local age_next = math.min(15, age + math.random(0, 1))
120 -- Select random type of fire spread
121 local burntype = math.random(1,2)
122 if burntype == 1 then
123 -- Spawn fire in air
124 local nodes = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"air"})
125 while #nodes > 0 do
126 local r = math.random(1, #nodes)
127 if minetest.find_node_near(nodes[r], 1, {"group:flammable"}) then
128 spawn_fire(nodes[r], age_next)
129 break
130 else
131 table.remove(nodes, r)
134 else
135 -- Replace flammable node with fire
136 local nodes = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"group:flammable"})
137 if #nodes > 0 then
138 local r = math.random(1, #nodes)
139 spawn_fire(nodes[r], age_next)
143 -- Regular age increase
144 if age + age_add <= 15 then
145 node.param2 = age + age_add
146 minetest.set_node(pos, node)
148 -- Restart timer
149 fire_timer(pos)
150 end,
151 drop = "",
152 sounds = {},
153 -- Turn into eternal fire on special blocks, light Nether portal (if possible), start burning timer
154 on_construct = function(pos)
155 local bpos = {x=pos.x, y=pos.y-1, z=pos.z}
156 local under = minetest.get_node(bpos).name
158 local dim = mcl_worlds.pos_to_dimension(bpos)
159 if under == "mcl_nether:magma" or under == "mcl_nether:netherrack" or (under == "mcl_core:bedrock" and dim == "end") then
160 minetest.swap_node(pos, {name = "mcl_fire:eternal_fire"})
163 if minetest.get_modpath("mcl_portals") then
164 mcl_portals.light_nether_portal(pos)
167 fire_timer(pos)
168 end,
169 _mcl_blast_resistance = 0,
172 minetest.register_node("mcl_fire:eternal_fire", {
173 description = S("Eternal Fire"),
174 _doc_items_longdesc = eternal_fire_help,
175 drawtype = "firelike",
176 tiles = {
178 name = "fire_basic_flame_animated.png",
179 animation = {
180 type = "vertical_frames",
181 aspect_w = 16,
182 aspect_h = 16,
183 length = 1
187 inventory_image = "fire_basic_flame.png",
188 paramtype = "light",
189 light_source = minetest.LIGHT_MAX,
190 walkable = false,
191 buildable_to = true,
192 sunlight_propagates = true,
193 damage_per_second = 1,
194 _mcl_node_death_message = fire_death_messages,
195 groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston = 1, destroys_items = 1},
196 floodable = true,
197 on_flood = function(pos, oldnode, newnode)
198 if minetest.get_item_group(newnode.name, "water") ~= 0 then
199 minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
201 end,
202 on_timer = function(pos)
203 if fire_enabled then
204 local airs = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"air"})
205 while #airs > 0 do
206 local r = math.random(1, #airs)
207 if minetest.find_node_near(airs[r], 1, {"group:flammable"}) then
208 spawn_fire(airs[r], age_next)
209 break
210 else
211 table.remove(airs, r)
215 -- Restart timer
216 fire_timer(pos)
217 end,
218 -- Start burning timer and light Nether portal (if possible)
219 on_construct = function(pos)
220 fire_timer(pos)
222 if minetest.get_modpath("mcl_portals") then
223 mcl_portals.light_nether_portal(pos)
225 end,
226 sounds = {},
227 drop = "",
228 _mcl_blast_resistance = 0,
232 -- Sound
235 if flame_sound then
237 local handles = {}
238 local timer = 0
240 -- Parameters
242 local radius = 8 -- Flame node search radius around player
243 local cycle = 3 -- Cycle time for sound updates
245 -- Update sound for player
247 function mcl_fire.update_player_sound(player)
248 local player_name = player:get_player_name()
249 -- Search for flame nodes in radius around player
250 local ppos = player:get_pos()
251 local areamin = vector.subtract(ppos, radius)
252 local areamax = vector.add(ppos, radius)
253 local fpos, num = minetest.find_nodes_in_area(
254 areamin,
255 areamax,
256 {"mcl_fire:fire", "mcl_fire:eternal_fire"}
258 -- Total number of flames in radius
259 local flames = (num["mcl_fire:fire"] or 0) +
260 (num["mcl_fire:eternal_fire"] or 0)
261 -- Stop previous sound
262 if handles[player_name] then
263 minetest.sound_fade(handles[player_name], -0.4, 0.0)
264 handles[player_name] = nil
266 -- If flames
267 if flames > 0 then
268 -- Find centre of flame positions
269 local fposmid = fpos[1]
270 -- If more than 1 flame
271 if #fpos > 1 then
272 local fposmin = areamax
273 local fposmax = areamin
274 for i = 1, #fpos do
275 local fposi = fpos[i]
276 if fposi.x > fposmax.x then
277 fposmax.x = fposi.x
279 if fposi.y > fposmax.y then
280 fposmax.y = fposi.y
282 if fposi.z > fposmax.z then
283 fposmax.z = fposi.z
285 if fposi.x < fposmin.x then
286 fposmin.x = fposi.x
288 if fposi.y < fposmin.y then
289 fposmin.y = fposi.y
291 if fposi.z < fposmin.z then
292 fposmin.z = fposi.z
295 fposmid = vector.divide(vector.add(fposmin, fposmax), 2)
297 -- Play sound
298 local handle = minetest.sound_play(
299 "fire_fire",
301 pos = fposmid,
302 to_player = player_name,
303 gain = math.min(0.06 * (1 + flames * 0.125), 0.18),
304 max_hear_distance = 32,
305 loop = true, -- In case of lag
308 -- Store sound handle for this player
309 if handle then
310 handles[player_name] = handle
315 -- Cycle for updating players sounds
317 minetest.register_globalstep(function(dtime)
318 timer = timer + dtime
319 if timer < cycle then
320 return
323 timer = 0
324 local players = minetest.get_connected_players()
325 for n = 1, #players do
326 mcl_fire.update_player_sound(players[n])
328 end)
330 -- Stop sound and clear handle on player leave
332 minetest.register_on_leaveplayer(function(player)
333 local player_name = player:get_player_name()
334 if handles[player_name] then
335 minetest.sound_stop(handles[player_name])
336 handles[player_name] = nil
338 end)
343 -- ABMs
346 -- Extinguish all flames quickly with water and such
348 minetest.register_abm({
349 label = "Extinguish fire",
350 nodenames = {"mcl_fire:fire", "mcl_fire:eternal_fire"},
351 neighbors = {"group:puts_out_fire"},
352 interval = 3,
353 chance = 1,
354 catch_up = false,
355 action = function(pos, node, active_object_count, active_object_count_wider)
356 minetest.remove_node(pos)
357 minetest.sound_play("fire_extinguish_flame",
358 {pos = pos, max_hear_distance = 16, gain = 0.15}, true)
359 end,
363 -- Enable the following ABMs according to 'enable fire' setting
365 if not fire_enabled then
367 -- Occasionally remove fire if fire disabled
368 -- NOTE: Fire is normally extinguished in timer function
369 minetest.register_abm({
370 label = "Remove disabled fire",
371 nodenames = {"mcl_fire:fire"},
372 interval = 10,
373 chance = 10,
374 catch_up = false,
375 action = minetest.remove_node,
378 else -- Fire enabled
380 -- Set fire to air nodes (inverse pyramid pattern) above lava source
381 minetest.register_abm({
382 label = "Ignite fire by lava",
383 nodenames = {"group:lava"},
384 interval = 7,
385 chance = 2,
386 catch_up = false,
387 action = function(pos)
388 local node = minetest.get_node(pos)
389 local def = minetest.registered_nodes[node.name]
390 -- Check if liquid source node
391 if def and def.liquidtype ~= "source" then
392 return
394 local function try_ignite(airs)
395 while #airs > 0 do
396 local r = math.random(1, #airs)
397 if minetest.find_node_near(airs[r], 1, {"group:flammable"}) then
398 spawn_fire(airs[r])
399 return true
400 else
401 table.remove(airs, r)
404 return false
406 local airs1 = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y+1, z=pos.z-1}, {x=pos.x+1, y=pos.y+1, z=pos.z+1}, {"air"})
407 local h = math.random(1, 2)
408 if h == 2 and #airs1 > 0 then
409 local airs2 = minetest.find_nodes_in_area({x=pos.x-2, y=pos.y+2, z=pos.z-2}, {x=pos.x+2, y=pos.y+2, z=pos.z+2}, {"air"})
410 try_ignite(airs2)
411 else
412 try_ignite(airs1)
414 end,
419 -- Set pointed_thing on (normal) fire.
420 -- * pointed_thing: Pointed thing to ignite
421 -- * player: Player who sets fire or nil if nobody
422 -- * allow_on_fire: If false, can't ignite fire on fire (default: true)
423 mcl_fire.set_fire = function(pointed_thing, player, allow_on_fire)
424 local pname
425 if player == nil then
426 pname = ""
427 else
428 pname = player:get_player_name()
430 local n = minetest.get_node(pointed_thing.above)
431 local nu = minetest.get_node(pointed_thing.under)
432 if allow_on_fire == false and minetest.get_item_group(nu.name, "fire") ~= 0 then
433 return
435 if minetest.is_protected(pointed_thing.above, pname) then
436 minetest.record_protection_violation(pointed_thing.above, pname)
437 return
439 if n.name == "air" then
440 minetest.add_node(pointed_thing.above, {name="mcl_fire:fire"})
444 minetest.register_alias("mcl_fire:basic_flame", "mcl_fire:fire")
445 minetest.register_alias("fire:basic_flame", "mcl_fire:fire")
446 minetest.register_alias("fire:permanent_flame", "mcl_fire:eternal_flame")
448 dofile(minetest.get_modpath(minetest.get_current_modname()).."/flint_and_steel.lua")
449 dofile(minetest.get_modpath(minetest.get_current_modname()).."/fire_charge.lua")