VoxeLibre/mods/ITEMS/mcl_fire/init.lua

567 lines
16 KiB
Lua
Raw Normal View History

-- Global namespace for functions
mcl_fire = {}
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
local has_mcl_portals = minetest.get_modpath("mcl_portals")
local set_node = minetest.set_node
local get_node = minetest.get_node
local add_node = minetest.add_node
local remove_node = minetest.remove_node
local swap_node = minetest.swap_node
local get_node_or_nil = minetest.get_node_or_nil
local find_nodes_in_area = minetest.find_nodes_in_area
local find_node_near = minetest.find_node_near
local get_item_group = minetest.get_item_group
local get_connected_players = minetest.get_connected_players
local vector = vector
local math = math
local adjacents = {
{ x =-1, y = 0, z = 0 },
{ x = 1, y = 0, z = 0 },
{ x = 0, y = 1, z = 0 },
{ x = 0, y =-1, z = 0 },
{ x = 0, y = 0, z =-1 },
{ x = 0, y = 0, z = 1 },
}
local function shuffle_table(t)
for i = #t, 1, -1 do
local r = math.random(i)
t[i], t[r] = t[r], t[i]
end
end
shuffle_table(adjacents)
local function has_flammable(pos)
for k,v in pairs(adjacents) do
local p=vector.add(pos,v)
local n=get_node_or_nil(p)
if n and get_item_group(n.name, "flammable") ~= 0 then
return p
end
end
end
local all_adjacents = {}
for x = 0,1 do
for y = 0,1 do
for z = 0,1 do
all_adjacents[#all_adjacents+1] = vector.new(x*2-1, y*2-1, z*2-1)
end
end
end
local function get_adjacent_fire_age(pos)
local lowest_age = nil
for k,v in pairs(all_adjacents) do
local p = vector.add(pos, v)
local node = get_node_or_nil(p)
if node and get_item_group(node.name, "fire") ~= 0 then
if not lowest_age or node.param2 < lowest_age then
lowest_age = node.param2
end
end
end
return lowest_age
end
2021-04-14 18:14:21 +02:00
local smoke_pdef = {
amount = 0.009,
maxexptime = 4.0,
minvel = { x = -0.1, y = 0.3, z = -0.1 },
maxvel = { x = 0.1, y = 1.6, z = 0.1 },
minsize = 4.0,
maxsize = 4.5,
2021-04-14 18:14:21 +02:00
minrelpos = { x = -0.45, y = -0.45, z = -0.45 },
maxrelpos = { x = 0.45, y = 0.45, z = 0.45 },
2021-03-14 19:57:13 +01:00
}
2020-08-19 19:27:59 +02:00
--
-- Items
--
-- Flame nodes
-- Fire settings
-- When enabled, fire destroys other blocks.
2019-04-09 15:25:27 +02:00
local fire_enabled = minetest.settings:get_bool("enable_fire", true)
-- Enable sound
2019-04-09 15:25:27 +02:00
local flame_sound = minetest.settings:get_bool("flame_sound", true)
-- Help texts
2019-04-09 15:25:27 +02:00
local fire_help, eternal_fire_help
if fire_enabled then
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.")
else
2019-03-15 04:36:17 +01:00
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.")
end
2019-04-09 15:25:27 +02:00
if fire_enabled then
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.")
else
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.")
end
-- exponential constant is such that at age=255, p=0.5%
local K1 = ( math.log(0.005) / math.log(10) ) / 255
local function spawn_fire(pos, age, force)
if not age then
minetest.log("warning","No age specified at "..debug.traceback())
-- Get adjacent age
local adjacent_age = get_adjacent_fire_age(pos)
-- Don't create new fire if we can't find adjacent fire from this position
if not adjacent_age then return end
age = adjacent_age + math.ceil(minetest.get_humidity(pos)/10) + math.random(5)
end
if age <= 1 then
2024-05-18 13:38:02 +02:00
minetest.log("warning","new flash point at "..vector.to_string(pos).." age="..tostring(age)..",backtrace = "..debug.traceback())
end
if age >= 255 then
age = 255
end
local node = get_node(pos)
local node_is_flammable = get_item_group(node.name, "flammable")
-- Limit fire spread
local probability_age = age
if node_is_flammable then
probability_age = probability_age * 0.80
end
local probability = math.pow(10,K1 * probability_age)
if not force and math.random(65536)/65536 >= probability then
return
end
set_node(pos, {name="mcl_fire:fire", param2 = age})
minetest.check_single_for_falling({x=pos.x, y=pos.y+1, z=pos.z})
end
minetest.register_node("mcl_fire:fire", {
description = S("Fire"),
_doc_items_longdesc = fire_help,
2017-01-05 04:50:26 +01:00
drawtype = "firelike",
tiles = {
{
name = "fire_basic_flame_animated.png",
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 1
},
},
},
2015-06-29 19:55:56 +02:00
inventory_image = "fire_basic_flame.png",
paramtype = "light",
2019-12-14 18:57:17 +01:00
light_source = minetest.LIGHT_MAX,
2015-06-29 19:55:56 +02:00
walkable = false,
buildable_to = true,
sunlight_propagates = true,
damage_per_second = 1,
2021-01-01 19:25:47 +01:00
groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston=1, destroys_items=1, set_on_fire=8},
2017-08-21 16:37:21 +02:00
floodable = true,
on_flood = function(pos, oldnode, newnode)
if get_item_group(newnode.name, "water") ~= 0 then
2020-04-07 00:55:45 +02:00
minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
2017-08-21 16:37:21 +02:00
end
end,
drop = "",
2017-02-19 22:54:06 +01:00
sounds = {},
-- Turn into eternal fire on special blocks, light Nether portal (if possible), start burning timer
on_construct = function(pos)
local bpos = {x=pos.x, y=pos.y-1, z=pos.z}
local under = get_node(bpos).name
local dim = mcl_worlds.pos_to_dimension(bpos)
if under == "mcl_nether:magma" or under == "mcl_nether:netherrack" or (under == "mcl_core:bedrock" and dim == "end") then
swap_node(pos, {name = "mcl_fire:eternal_fire"})
end
if has_mcl_portals then
mcl_portals.light_nether_portal(pos)
end
2021-04-14 18:14:21 +02:00
mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
2020-08-19 19:27:59 +02:00
end,
on_destruct = function(pos)
mcl_particles.delete_node_particlespawners(pos)
2015-06-29 19:55:56 +02:00
end,
_mcl_blast_resistance = 0,
2015-06-29 19:55:56 +02:00
})
minetest.register_node("mcl_fire:eternal_fire", {
description = S("Eternal Fire"),
_doc_items_longdesc = eternal_fire_help,
drawtype = "firelike",
tiles = {
{
name = "fire_basic_flame_animated.png",
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 1
},
},
},
inventory_image = "fire_basic_flame.png",
paramtype = "light",
2019-12-14 18:57:17 +01:00
light_source = minetest.LIGHT_MAX,
walkable = false,
buildable_to = true,
sunlight_propagates = true,
damage_per_second = 1,
2021-01-01 19:25:47 +01:00
groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston = 1, destroys_items = 1, set_on_fire=8},
2017-08-21 16:37:21 +02:00
floodable = true,
on_flood = function(pos, oldnode, newnode)
if get_item_group(newnode.name, "water") ~= 0 then
2020-04-07 00:55:45 +02:00
minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
2017-08-21 16:37:21 +02:00
end
end,
-- Start burning timer and light Nether portal (if possible)
on_construct = function(pos)
if has_mcl_portals then --Calling directly minetest.get_modpath consumes 4x more compute time
mcl_portals.light_nether_portal(pos)
end
2021-04-14 18:14:21 +02:00
mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
2020-08-19 19:27:59 +02:00
end,
on_destruct = function(pos)
mcl_particles.delete_node_particlespawners(pos)
end,
2017-02-19 22:54:06 +01:00
sounds = {},
drop = "",
_mcl_blast_resistance = 0,
})
--
-- Sound
--
if flame_sound then
local handles = {}
local timer = 0
-- Parameters
local radius = 8 -- Flame node search radius around player
local cycle = 3 -- Cycle time for sound updates
-- Update sound for player
function mcl_fire.update_player_sound(player)
local player_name = player:get_player_name()
-- Search for flame nodes in radius around player
2019-02-01 06:33:07 +01:00
local ppos = player:get_pos()
local areamin = vector.subtract(ppos, radius)
local areamax = vector.add(ppos, radius)
local fpos, num = find_nodes_in_area(
areamin,
areamax,
{"mcl_fire:fire", "mcl_fire:eternal_fire"}
)
-- Total number of flames in radius
local flames = (num["mcl_fire:fire"] or 0) +
(num["mcl_fire:eternal_fire"] or 0)
-- Stop previous sound
if handles[player_name] then
2019-09-05 00:07:32 +02:00
minetest.sound_fade(handles[player_name], -0.4, 0.0)
handles[player_name] = nil
2015-06-29 19:55:56 +02:00
end
-- If flames
if flames > 0 then
-- Find centre of flame positions
local fposmid = fpos[1]
-- If more than 1 flame
if #fpos > 1 then
local fposmin = areamax
local fposmax = areamin
for i = 1, #fpos do
local fposi = fpos[i]
if fposi.x > fposmax.x then
fposmax.x = fposi.x
end
if fposi.y > fposmax.y then
fposmax.y = fposi.y
end
if fposi.z > fposmax.z then
fposmax.z = fposi.z
end
if fposi.x < fposmin.x then
fposmin.x = fposi.x
end
if fposi.y < fposmin.y then
fposmin.y = fposi.y
end
if fposi.z < fposmin.z then
fposmin.z = fposi.z
end
end
fposmid = vector.divide(vector.add(fposmin, fposmax), 2)
end
-- Play sound
local handle = minetest.sound_play(
"fire_fire",
{
pos = fposmid,
to_player = player_name,
gain = math.min(0.06 * (1 + flames * 0.125), 0.18),
max_hear_distance = 32,
loop = true, -- In case of lag
}
)
-- Store sound handle for this player
if handle then
handles[player_name] = handle
end
2015-06-29 19:55:56 +02:00
end
end
-- Cycle for updating players sounds
2015-06-29 19:55:56 +02:00
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer < cycle then
return
end
2015-06-29 19:55:56 +02:00
timer = 0
local players = get_connected_players()
for n = 1, #players do
mcl_fire.update_player_sound(players[n])
end
end)
-- Stop sound and clear handle on player leave
2015-06-29 19:55:56 +02:00
minetest.register_on_leaveplayer(function(player)
local player_name = player:get_player_name()
if handles[player_name] then
minetest.sound_stop(handles[player_name])
handles[player_name] = nil
end
end)
2015-06-29 19:55:56 +02:00
end
-- [...]a fire that is not adjacent to any flammable block does not spread, even to another flammable block within the normal range.
-- https://minecraft.fandom.com/wiki/Fire#Spread
local function check_aircube(p1,p2)
local nds=minetest.find_nodes_in_area(p1,p2,{"air"})
shuffle_table(nds)
for k,v in pairs(nds) do
if has_flammable(v) then return v end
end
end
-- [...] a fire block can turn any air block that is adjacent to a flammable block into a fire block. This can happen at a distance of up to one block downward, one block sideways (including diagonals), and four blocks upward of the original fire block (not the block the fire is on/next to).
local function get_ignitable(pos)
return check_aircube(vector.add(pos,vector.new(-1,-1,-1)),vector.add(pos,vector.new(1,4,1)))
end
-- Fire spreads from a still lava block similarly: any air block one above and up to one block sideways (including diagonals) or two above and two blocks sideways (including diagonals) that is adjacent to a flammable block may be turned into a fire block.
local function get_ignitable_by_lava(pos)
return check_aircube(vector.add(pos,vector.new(-1,1,-1)),vector.add(pos,vector.new(1,1,1))) or check_aircube(vector.add(pos,vector.new(-2,2,-2)),vector.add(pos,vector.new(2,2,2))) or nil
end
--
-- ABMs
--
-- Extinguish all flames quickly with water and such
2015-06-29 19:55:56 +02:00
minetest.register_abm({
2020-04-22 01:31:30 +02:00
label = "Extinguish fire",
nodenames = {"mcl_fire:fire", "mcl_fire:eternal_fire"},
neighbors = {"group:puts_out_fire"},
interval = 3,
chance = 1,
catch_up = false,
action = function(pos, node, active_object_count, active_object_count_wider)
minetest.remove_node(pos)
minetest.sound_play("fire_extinguish_flame",
2020-04-07 00:55:45 +02:00
{pos = pos, max_hear_distance = 16, gain = 0.15}, true)
2015-06-29 19:55:56 +02:00
end,
})
-- Enable the following ABMs according to 'enable fire' setting
if not fire_enabled then
2019-04-09 15:25:27 +02:00
-- Occasionally remove fire if fire disabled
-- NOTE: Fire is normally extinguished in timer function
minetest.register_abm({
label = "Remove disabled fire",
2017-05-20 03:24:36 +02:00
nodenames = {"mcl_fire:fire"},
2019-04-09 15:25:27 +02:00
interval = 10,
chance = 10,
catch_up = false,
action = minetest.remove_node,
})
2019-04-09 15:25:27 +02:00
else -- Fire enabled
-- Extinguish parameters
local C2 = math.log(1/20) / math.log(10)
local K2 = -C2 / 255
-- Fire Spread
minetest.register_abm({
label = "Ignite flame",
nodenames ={"mcl_fire:fire","mcl_fire:eternal_fire"},
interval = 7,
chance = 5,
catch_up = false,
action = function(pos)
local node = get_node(pos)
2024-05-18 13:49:11 +02:00
local age = node.param2
-- Always age the source fire
age = age + math.ceil(minetest.get_humidity(pos)/10) + math.random(5)
if age > 255 then age = 255 end
node.param2 = age
local p = get_ignitable(pos)
if p then
-- Spawn new fire with an age based on this node's age
spawn_fire(p, age + math.ceil(minetest.get_humidity(p)/10) + math.random(5))
shuffle_table(adjacents)
2024-05-18 13:49:11 +02:00
end
if node.name ~= "mcl_fire:eternal_fire" then
-- Randomly extinguish fires with increasing probability the older they are
local extinguish_probability = math.pow(10,K2 * age + C2)
if math.random(65536)/65536 <= extinguish_probability then
node.name = "air"
node.param2 = 0
-- Extinguish fires not adjacent to flammable materials
elseif not has_flammable(pos) then
node.name = "air"
node.param2 = 0
end
end
set_node(pos, node)
end
})
--lava fire spread
2017-05-20 03:24:36 +02:00
minetest.register_abm({
label = "Ignite fire by lava",
nodenames = {"mcl_core:lava_source","mcl_nether:nether_lava_source"},
2022-11-02 13:03:29 +01:00
neighbors = {"group:flammable"},
interval = 15,
chance = 9,
2017-05-20 03:24:36 +02:00
catch_up = false,
action = function(pos)
local p=get_ignitable_by_lava(pos)
if p then
spawn_fire(p, 0)
2017-05-20 03:24:36 +02:00
end
end,
})
-- Remove flammable nodes around basic flame
minetest.register_abm({
label = "Remove flammable nodes",
nodenames = {"mcl_fire:fire","mcl_fire:eternal_fire"},
neighbors = {"group:flammable"},
interval = 5,
chance = 18,
catch_up = false,
action = function(pos)
local p = has_flammable(pos)
if not p then
return
end
local node = get_node(p)
local node_name = node.name
local def = minetest.registered_nodes[node_name]
local fgroup = minetest.get_item_group(node_name, "flammable")
if def and def._on_burn then
def._on_burn(p)
elseif fgroup ~= -1 then
local source_node = get_node(pos)
local age = source_node.param2
spawn_fire(p, age + math.ceil(minetest.get_humidity(p)/10) + math.random(5), true)
minetest.check_for_falling(p)
if source_node.name == "mcl_fire:fire" then
-- Always age the source fire
age = age + math.ceil(minetest.get_humidity(pos)/10) + math.random(5)
if age > 255 then age = 255 end
source_node.param2 = age
set_node(pos, source_node)
end
end
end
})
end
2017-02-01 16:43:05 +01:00
-- Set pointed_thing on (normal) fire.
-- * pointed_thing: Pointed thing to ignite
-- * player: Player who sets fire or nil if nobody
2020-03-24 19:53:08 +01:00
-- * allow_on_fire: If false, can't ignite fire on fire (default: true)
2021-05-25 12:52:25 +02:00
function mcl_fire.set_fire(pointed_thing, player, allow_on_fire)
local pname
if player == nil then
pname = ""
else
pname = player:get_player_name()
end
if minetest.is_protected(pointed_thing.above, pname) then
minetest.record_protection_violation(pointed_thing.above, pname)
return
end
local n_pointed = get_node(pointed_thing.under)
if allow_on_fire == false and get_item_group(n_pointed.name, "fire") ~= 0 then
return
end
local n_fire_pos = get_node(pointed_thing.above)
if n_fire_pos.name ~= "air" then
return
end
local n_below = get_node(vector.offset(pointed_thing.above, 0, -1, 0))
if minetest.get_item_group(n_below.name, "water") ~= 0 then
return
2017-02-01 16:43:05 +01:00
end
return add_node(pointed_thing.above, {name="mcl_fire:fire"})
2017-02-01 16:43:05 +01:00
end
2020-08-19 19:27:59 +02:00
minetest.register_lbm({
label = "Smoke particles from fire",
name = "mcl_fire:smoke",
nodenames = {"group:fire"},
run_at_every_load = true,
action = function(pos, node)
2021-04-14 18:14:21 +02:00
mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
2020-08-19 19:27:59 +02:00
end,
})
minetest.register_alias("mcl_fire:basic_flame", "mcl_fire:fire")
minetest.register_alias("fire:basic_flame", "mcl_fire:fire")
minetest.register_alias("fire:permanent_flame", "mcl_fire:eternal_fire")
2017-02-01 16:43:05 +01:00
dofile(modpath.."/flint_and_steel.lua")
dofile(modpath.."/fire_charge.lua")