--[[
    Everness. Never ending discovery in Everness mapgen.
    Copyright (C) 2024 SaKeL

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

--]]

--
-- Convert dirt to something that fits the environment
--

local grass_covered_mapping = {
    ['everness:coral_dirt'] = { 'everness:dirt_with_coral_grass' },
    ['everness:cursed_dirt'] = { 'everness:dirt_with_cursed_grass' },
    ['everness:crystal_dirt'] = { 'everness:dirt_with_crystal_grass' },
    ['everness:dirt_1'] = {
        'everness:dirt_with_grass_1',
        'everness:dirt_with_grass_extras_1',
        'everness:dirt_with_grass_extras_2',
    },
    ['everness:dry_dirt'] = { 'everness:dry_dirt_with_dry_grass' }
}

local grass_covered_mapping_under = {
    ['everness:coral_desert_stone'] = { 'everness:coral_desert_stone_with_moss' },
    ['everness:soul_sandstone'] = { 'everness:soul_sandstone_veined' },
    ['everness:crystal_cave_dirt'] = { 'everness:crystal_cave_dirt_with_moss' },
    ['everness:mold_cobble'] = { 'everness:mold_stone_with_moss' },
}

-- Spread grass on dirt

minetest.register_abm({
    label = 'Everness Grass spread',
    nodenames = {
        'everness:coral_dirt',
        'everness:cursed_dirt',
        'everness:crystal_dirt',
        'everness:dirt_1',
        'everness:dry_dirt',
    },
    neighbors = {
        'air',
        'group:coral_grass',
        'group:cursed_grass',
        'group:crystal_grass',
        'group:bamboo_grass',
        'group:everness_dry_grass'
    },
    interval = 6,
    chance = 50,
    catch_up = false,
    action = function(pos, node)
        -- Check for darkness: night, shadow or under a light-blocking node
        -- Returns if ignore above
        local above = { x = pos.x, y = pos.y + 1, z = pos.z }
        if (minetest.get_node_light(above) or 0) < 13 then
            return
        end

        -- Look for spreading dirt-type neighbours
        local p2 = minetest.find_node_near(pos, 1, 'group:everness_spreading_dirt_type')

        if p2 then
            local n3_def = grass_covered_mapping[node.name]

            if not n3_def then
                return
            end

            local n3_name = n3_def[1]

            if #n3_def > 1 then
                n3_name = n3_def[math.random(1, #n3_def)]
            end

            minetest.set_node(pos, { name = n3_name })
            return
        end

        -- Else, any seeding nodes on top?
        local name = minetest.get_node(above).name

        if minetest.get_item_group(name, 'coral_grass') ~= 0 and node.name == 'everness:coral_dirt' then
            minetest.set_node(pos, { name = 'everness:dirt_with_coral_grass' })
        elseif minetest.get_item_group(name, 'cursed_grass') ~= 0 and node.name == 'everness:cursed_dirt' then
            minetest.set_node(pos, { name = 'everness:dirt_with_cursed_grass' })
        elseif minetest.get_item_group(name, 'crystal_grass') ~= 0 and node.name == 'everness:crystal_dirt' then
            minetest.set_node(pos, { name = 'everness:dirt_with_crystal_grass' })
        elseif minetest.get_item_group(name, 'bamboo_grass') ~= 0 and node.name == 'everness:dirt_1' then
            local bamboo_grass_covered_types = {
                'everness:dirt_with_grass_1',
                'everness:dirt_with_grass_extras_1',
                'everness:dirt_with_grass_extras_2'
            }

            minetest.set_node(pos, { name = bamboo_grass_covered_types[math.random(1, #bamboo_grass_covered_types)] })
        elseif minetest.get_item_group(name, 'everness_dry_grass') ~= 0 and node.name == 'everness:dry_dirt' then
            minetest.set_node(pos, { name = 'everness:dry_dirt_with_dry_grass' })
        end
    end
})

-- Spread mold/moss on stone/dirt - under

minetest.register_abm({
    label = 'Everness Grass spread under',
    nodenames = {
        'everness:coral_desert_stone',
        'everness:soul_sandstone',
        'everness:crystal_cave_dirt',
        'everness:mold_cobble',
    },
    neighbors = {
        'air',
        'group:coral_grass_under',
        'group:cursed_grass_under',
        'group:crystal_grass_under',
        'group:forsaken_tundra_grass_under',
    },
    interval = 6,
    chance = 50,
    catch_up = false,
    action = function(pos, node)
        -- Check for darkness: night, shadow or under a light-blocking node
        -- Returns if ignore above
        local above = { x = pos.x, y = pos.y + 1, z = pos.z }
        if (minetest.get_node_light(above) or 0) < 13 then
            return
        end

        -- Look for spreading dirt-type neighbours
        local p2 = minetest.find_node_near(pos, 1, 'group:everness_spreading_dirt_type_under')

        if p2 then
            local n3_def = grass_covered_mapping_under[node.name]

            if not n3_def then
                return
            end

            local n3_name = n3_def[1]

            if #n3_def > 1 then
                n3_name = n3_def[math.random(1, #n3_def)]
            end

            minetest.set_node(pos, {name = n3_name})
            return
        end

        -- Else, any seeding nodes on top?
        local name = minetest.get_node(above).name

        if minetest.get_item_group(name, 'coral_grass_under') ~= 0 and node.name == 'everness:coral_desert_stone' then
            minetest.set_node(pos, { name = 'everness:coral_desert_stone_with_moss' })
        elseif minetest.get_item_group(name, 'cursed_grass_under') ~= 0 and node.name == 'everness:soul_sandstone' then
            minetest.set_node(pos, { name = 'everness:soul_sandstone_veined' })
        elseif minetest.get_item_group(name, 'crystal_grass_under') ~= 0 and node.name == 'everness:crystal_cave_dirt' then
            minetest.set_node(pos, { name = 'everness:crystal_cave_dirt_with_moss' })
        elseif minetest.get_item_group(name, 'forsaken_tundra_grass_under') ~= 0 and node.name == 'everness:mold_cobble' then
            minetest.set_node(pos, { name = 'everness:mold_stone_with_moss' })
        end
    end
})

--
-- Grass and dry grass removed in darkness
--

minetest.register_abm({
    label = 'Everness Grass covered',
    nodenames = {
        'group:everness_spreading_dirt_type',
        'group:everness_spreading_dirt_type_under',
    },
    interval = 8,
    chance = 50,
    catch_up = false,
    action = function(pos, node)
        local above = { x = pos.x, y = pos.y + 1, z = pos.z }
        local name = minetest.get_node(above).name
        local nodedef = minetest.registered_nodes[name]

        if name ~= 'ignore'
            and nodedef
            and not (
                (nodedef.sunlight_propagates or nodedef.paramtype == 'light')
                and nodedef.liquidtype == 'none'
            )
        then
            if node.name == 'everness:dirt_with_coral_grass' then
                minetest.set_node(pos, { name = 'everness:coral_dirt' })
            elseif node.name == 'everness:dirt_with_cursed_grass' then
                minetest.set_node(pos, { name = 'everness:cursed_dirt' })
            elseif node.name == 'everness:dirt_with_crystal_grass' then
                minetest.set_node(pos, { name = 'everness:crystal_dirt' })
            elseif node.name == 'everness:dirt_with_grass_1'
                or node.name == 'everness:dirt_with_grass_extras_1'
                or node.name == 'everness:dirt_with_grass_extras_2'
            then
                minetest.set_node(pos, { name = 'everness:dirt_1' })
            elseif node.name == 'everness:coral_desert_stone_with_moss' then
                minetest.set_node(pos, { name = 'everness:coral_desert_stone' })
            elseif node.name == 'everness:dry_dirt_with_dry_grass' then
                minetest.set_node(pos, { name = 'everness:dry_dirt' })
            elseif node.name == 'everness:soul_sandstone_veined' then
                minetest.set_node(pos, { name = 'everness:soul_sandstone' })
            elseif node.name == 'everness:crystal_cave_dirt_with_moss' then
                minetest.set_node(pos, { name = 'everness:crystal_cave_dirt' })
            elseif node.name == 'everness:mold_stone_with_moss' then
                minetest.set_node(pos, { name = 'everness:mold_cobble' })
            end
        end
    end
})

--
-- Leafdecay
--

Everness:register_leafdecay({
    trunks = {
        'everness:coral_tree',
        'everness:crystal_bush_stem',
        'everness:cursed_bush_stem',
        'everness:willow_tree',
        'everness:sequoia_tree',
        'everness:mese_tree',
        'everness:palm_tree'
    },
    leaves = {
        'everness:coral_leaves',
        'everness:crystal_bush_leaves',
        'everness:willow_leaves',
        'everness:sequoia_leaves',
        'everness:mese_leaves',
        'everness:mese_tree_fruit',
        'everness:palm_leaves',
        'everness:coconut'
    },
    radius = 3
})

-- Baobab Tree
Everness:register_leafdecay({
    trunks = {
        'everness:baobab_tree',
        'everness:crystal_tree',
        'everness:dry_tree',
    },
    leaves = {
        'everness:baobab_leaves',
        'everness:baobab_fruit_renewable',
        'everness:crystal_leaves',
        'everness:dry_branches',
    },
    radius = 4,
})

--
-- Moss growth on cobble near water
--

local moss_correspondences = {
    ['everness:coral_desert_cobble'] = 'everness:coral_desert_mossy_cobble',
    ['everness:crystal_cobble'] = 'everness:crystal_mossy_cobble',
}

local moss_nodenames_correspondences = {
    'everness:coral_desert_cobble',
    'everness:crystal_cobble',
}

if minetest.get_modpath('default') then
    moss_correspondences['stairs:slab_coral_desert_cobble'] = 'stairs:slab_coral_desert_mossy_cobble'
    moss_correspondences['stairs:stair_coral_desert_cobble'] = 'stairs:stair_coral_desert_mossy_cobble'
    moss_correspondences['stairs:stair_inner_coral_desert_cobble'] = 'stairs:stair_inner_coral_desert_mossy_cobble'
    moss_correspondences['stairs:stair_outer_coral_desert_cobble'] = 'stairs:stair_outer_coral_desert_mossy_cobble'
    moss_correspondences['everness:coral_desert_cobble_wall'] = 'everness:coral_desert_mossy_cobble_wall'

    table.insert(moss_nodenames_correspondences, 'stairs:slab_coral_desert_cobble')
    table.insert(moss_nodenames_correspondences, 'stairs:stair_coral_desert_cobble')
    table.insert(moss_nodenames_correspondences, 'stairs:stair_inner_coral_desert_cobble')
    table.insert(moss_nodenames_correspondences, 'stairs:stair_outer_coral_desert_cobble')
    table.insert(moss_nodenames_correspondences, 'everness:coral_desert_cobble_wall')
end

minetest.register_abm({
    label = 'Everness Moss growth',
    nodenames = moss_nodenames_correspondences,
    neighbors = { 'group:water' },
    interval = 16,
    chance = 200,
    catch_up = false,
    action = function(pos, node)
        node.name = moss_correspondences[node.name]

        if node.name then
            minetest.set_node(pos, node)
        end
    end
})

--
-- Magma growth on cobble near lava
--

local magma_correspondences = {
    ['everness:volcanic_rock'] = 'everness:volcanic_rock_with_magma',
}

local magma_nodenames_correspondences = {
    'everness:volcanic_rock'
}

if minetest.get_modpath('default') then
    magma_correspondences['default:cobble'] = 'everness:magmacobble'
    magma_correspondences['stairs:slab_cobble'] = 'stairs:slab_magmacobble'
    magma_correspondences['stairs:stair_cobble'] = 'stairs:stair_magmacobble'
    magma_correspondences['stairs:stair_inner_cobble'] = 'stairs:stair_inner_magmacobble'
    magma_correspondences['stairs:stair_outer_cobble'] = 'stairs:stair_outer_magmacobble'
    magma_correspondences['walls:cobble'] = 'everness:magmacobble_wall'
    magma_correspondences['stairs:slab_volcanic_rock'] = 'stairs:slab_volcanic_rock_with_magma'
    magma_correspondences['stairs:stair_volcanic_rock'] = 'stairs:stair_volcanic_rock_with_magma'
    magma_correspondences['stairs:stair_inner_volcanic_rock'] = 'stairs:stair_inner_volcanic_rock_with_magma'
    magma_correspondences['stairs:stair_outer_volcanic_rock'] = 'stairs:stair_outer_volcanic_rock_with_magma'
    magma_correspondences['everness:volcanic_rock_wall'] = 'everness:volcanic_rock_with_magma_wall'

    table.insert(magma_nodenames_correspondences, 'default:cobble')
    table.insert(magma_nodenames_correspondences, 'stairs:slab_cobble')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_cobble')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_inner_cobble')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_outer_cobble')
    table.insert(magma_nodenames_correspondences, 'walls:cobble')
    table.insert(magma_nodenames_correspondences, 'stairs:slab_volcanic_rock')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_volcanic_rock')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_inner_volcanic_rock')
    table.insert(magma_nodenames_correspondences, 'stairs:stair_outer_volcanic_rock')
    table.insert(magma_nodenames_correspondences, 'everness:volcanic_rock_wall')
end

minetest.register_abm({
    label = 'Everness Magma growth',
    nodenames = magma_nodenames_correspondences,
    neighbors = { 'group:lava' },
    interval = 16,
    chance = 200,
    catch_up = false,
    action = function(pos, node)
        node.name = magma_correspondences[node.name]

        if node.name then
            minetest.set_node(pos, node)
        end
    end
})

--
-- Falling leaves
--

minetest.register_abm({
    label = 'everness:falling_leaves',
    nodenames = { 'group:falling_leaves' },
    neighbors = { 'air' },
    interval = 16,
    chance = 16,
    catch_up = false,
    action = function(pos, node)
        if not minetest.settings:get_bool('enable_particles', true) then
            return
        end

        local air_below = minetest.find_nodes_in_area(pos, { x = pos.x, y = pos.y + 3, z = pos.z }, { 'air' })

        if #air_below < 3 then
            return
        end

        -- particles
        local particlespawner_def = {
            amount = 5,
            time = 1,
            minpos = { x = pos.x - 0.5, y = pos.y - 0.5, z = pos.z - 0.5 },
            maxpos = { x = pos.x + 0.5, y = pos.y - 0.5, z = pos.z + 0.5 },
            minvel = { x = -0.25, y = -0.25, z = -0.25 },
            maxvel = { x = 0.25, y = -0.5, z = 0.25 },
            minacc = { x = -0.25, y = -0.25, z = -0.25 },
            maxacc = { x = 0.25, y = -0.5, z = 0.25 },
            minexptime = 3,
            maxexptime = 6,
            minsize = 0.5,
            maxsize = 1.5,
            node = node
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, after v5.6.0
            particlespawner_def = {
                amount = 5,
                time = 1,
                size = {
                    min = 0.5,
                    max = 1.5,
                },
                exptime = {
                    min = 3,
                    max = 6
                },
                pos = {
                    min = vector.new({ x = pos.x - 0.5, y = pos.y - 0.5, z = pos.z - 0.5 }),
                    max = vector.new({ x = pos.x + 0.5, y = pos.y - 0.5, z = pos.z + 0.5 }),
                },
                vel = {
                    min = vector.new({ x = -0.25, y = -0.25, z = -0.25 }),
                    max = vector.new({ x = 0.25, y = -0.5, z = 0.25 })
                },
                acc = {
                    min = vector.new({ x = -0.25, y = -0.25, z = -0.25 }),
                    max = vector.new({ x = 0.25, y = -0.5, z = 0.25 })
                },
                node = {
                    name = node.name
                }
            }
        end

        minetest.add_particlespawner(particlespawner_def)
    end
})

--
-- Falling leaves - vines
--

minetest.register_abm({
    label = 'everness:falling_vines',
    nodenames = { 'group:falling_vines' },
    neighbors = { 'air' },
    interval = 16,
    chance = 16,
    catch_up = false,
    action = function(pos, node)
        if not minetest.settings:get_bool('enable_particles', true) then
            return
        end

        local air_around = minetest.find_nodes_in_area(
            { x = pos.x - 1, y = pos.y, z = pos.z - 1 },
            { x = pos.x + 1, y = pos.y, z = pos.z + 1 },
            { 'air' }
        )

        if #air_around < 3 then
            return
        end

        -- particles
        local particlespawner_def = {
            amount = 5,
            time = 1,
            minpos = { x = pos.x - 0.5, y = pos.y - 0.5, z = pos.z - 0.5 },
            maxpos = { x = pos.x + 0.5, y = pos.y - 0.5, z = pos.z + 0.5 },
            minvel = { x = -0.25, y = -0.25, z = -0.25 },
            maxvel = { x = 0.25, y = -0.5, z = 0.25 },
            minacc = { x = -0.25, y = -0.25, z = -0.25 },
            maxacc = { x = 0.25, y = -0.5, z = 0.25 },
            minexptime = 15,
            maxexptime = 30,
            minsize = 0.5,
            maxsize = 1.5,
            node = node,
            glow = 7
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, after v5.6.0
            particlespawner_def = {
                amount = 5,
                time = 1,
                size = {
                    min = 0.5,
                    max = 1.5,
                },
                exptime = {
                    min = 15,
                    max = 30
                },
                pos = {
                    min = vector.new(pos.x - 0.5, pos.y - 0.5, pos.z - 0.5),
                    max = vector.new(pos.x + 0.5, pos.y - 0.5, pos.z + 0.5),
                },
                vel = {
                    min = vector.new(-0.25, -0.15, -0.25),
                    max = vector.new(0.25, -0.25, 0.25)
                },
                acc = {
                    min = vector.new(-0.25, -0.05, -0.25),
                    max = vector.new(0.25, -0.1, 0.25)
                },
                node = {
                    name = node.name
                },
                glow = 7
            }
        end

        minetest.add_particlespawner(particlespawner_def)
    end
})

minetest.register_abm({
    label = 'Grow orange cactus',
    nodenames = {
        'everness:cactus_orange',
        'everness:cactus_blue'
    },
    neighbors = { 'group:sand', 'group:everness_sand' },
    interval = 12,
    chance = 83,
    action = function(...)
        Everness:grow_cactus(...)
    end
})

--
-- Bio Bubbles
--

minetest.register_abm({
    label = 'everness:bio_bubbles',
    nodenames = { 'group:bio_bubbles' },
    neighbors = { 'group:water' },
    interval = 16,
    chance = 2,
    catch_up = false,
    action = function(pos, node)
        if not minetest.settings:get_bool('enable_particles', true) then
            return
        end

        local water_above = minetest.find_nodes_in_area(pos, { x = pos.x, y = pos.y + 10, z = pos.z }, { 'group:water' })

        if #water_above < 10 then
            return
        end

        -- particles
        local particlespawner_def = {
            amount = 50,
            time = 10,
            minpos = vector.new({ x = pos.x - 0.1, y = pos.y + 0.6, z = pos.z - 0.1 }),
            maxpos = vector.new({ x = pos.x + 0.1, y = pos.y + 0.6, z = pos.z + 0.1 }),
            minvel = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxvel = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minacc = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxacc = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minexptime = 5,
            maxexptime = 7,
            minsize = 2,
            maxsize = 3.5,
            texture = 'everness_bubble.png',
            glow = 7
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, after v5.6.0
            particlespawner_def = {
                amount = 50,
                time = 10,
                size = {
                    min = 2,
                    max = 3.5,
                },
                exptime = {
                    min = 5,
                    max = 7
                },
                pos = {
                    min = vector.new({ x = pos.x - 0.1, y = pos.y + 0.6, z = pos.z - 0.1 }),
                    max = vector.new({ x = pos.x + 0.1, y = pos.y + 0.6, z = pos.z + 0.1 }),
                },
                vel = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                acc = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                texture = {
                    name = 'everness_bubble.png',
                    alpha_tween = {
                        1, 0,
                        style = 'fwd',
                        reps = 1
                    },
                    scale_tween = {
                        0.5, 1,
                        style = 'fwd',
                        reps = 1
                    }
                },
                glow = 7
            }
        end

        minetest.add_particlespawner(particlespawner_def)
    end
})

--
-- Rising Souls
--

minetest.register_abm({
    label = 'everness:rising_souls',
    nodenames = { 'group:rising_souls' },
    neighbors = { 'group:water' },
    interval = 16,
    chance = 2,
    catch_up = false,
    action = function(pos, node)
        if not minetest.settings:get_bool('enable_particles', true) then
            return
        end

        local water_above = minetest.find_nodes_in_area(pos, { x = pos.x, y = pos.y + 10, z = pos.z }, { 'group:water' })

        if #water_above < 10 then
            return
        end

        -- particles
        local particlespawner_def = {
            amount = 17,
            time = 10,
            minpos = vector.new({ x = pos.x - 0.3, y = pos.y + 0.6, z = pos.z - 0.3 }),
            maxpos = vector.new({ x = pos.x + 0.3, y = pos.y + 0.6, z = pos.z + 0.3 }),
            minvel = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxvel = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minacc = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxacc = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minexptime = 4,
            maxexptime = 6,
            minsize = 4,
            maxsize = 6,
            texture = 'everness_rising_soul_particle.png',
            glow = 7
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, after v5.6.0
            particlespawner_def = {
                amount = 17,
                time = 10,
                size = {
                    min = 4,
                    max = 6,
                },
                exptime = {
                    min = 4,
                    max = 6
                },
                pos = {
                    min = vector.new({ x = pos.x - 0.3, y = pos.y + 0.6, z = pos.z - 0.3 }),
                    max = vector.new({ x = pos.x + 0.3, y = pos.y + 0.6, z = pos.z + 0.3 }),
                },
                vel = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                acc = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                texture = {
                    name = 'everness_rising_soul_particle.png',
                    animation = {
                        type = 'vertical_frames',
                        aspect_w = 16,
                        aspect_h = 16,
                        length = 2,
                    },
                    alpha_tween = {
                        1, 0,
                        style = 'fwd',
                        reps = 1
                    },
                    scale_tween = {
                        0.5, 1,
                        style = 'fwd',
                        reps = 1
                    }
                },
                glow = 7
            }
        end

        minetest.add_particlespawner(particlespawner_def)
    end
})

--
-- Rising Crystals
--

minetest.register_abm({
    label = 'everness:rising_crystals',
    nodenames = { 'group:rising_crystals' },
    neighbors = { 'group:water' },
    interval = 16,
    chance = 2,
    catch_up = false,
    action = function(pos, node)
        if not minetest.settings:get_bool('enable_particles', true) then
            return
        end

        local water_above = minetest.find_nodes_in_area(pos, { x = pos.x, y = pos.y + 10, z = pos.z }, { 'group:water' })

        if #water_above < 10 then
            return
        end

        -- particles
        local particlespawner_def = {
            amount = 17,
            time = 10,
            minpos = vector.new({ x = pos.x - 0.3, y = pos.y + 0.6, z = pos.z - 0.3 }),
            maxpos = vector.new({ x = pos.x + 0.3, y = pos.y + 0.6, z = pos.z + 0.3 }),
            minvel = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxvel = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minacc = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
            maxacc = vector.new({ x = 0.1, y = 0.5, z = 0.1 }),
            minexptime = 4,
            maxexptime = 6,
            minsize = 4,
            maxsize = 6,
            texture = 'everness_rising_soul_particle.png',
            glow = 7
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, after v5.6.0
            particlespawner_def = {
                amount = 25,
                time = 10,
                size = {
                    min = 6,
                    max = 8,
                },
                exptime = {
                    min = 4,
                    max = 6
                },
                pos = {
                    min = vector.new({ x = pos.x - 0.3, y = pos.y + 0.6, z = pos.z - 0.3 }),
                    max = vector.new({ x = pos.x + 0.3, y = pos.y + 0.6, z = pos.z + 0.3 }),
                },
                vel = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                acc = {
                    min = vector.new({ x = -0.1, y = 0.25, z = -0.1 }),
                    max = vector.new({ x = 0.1, y = 0.5, z = 0.1 })
                },
                texture = {
                    name = 'everness_crystal_forest_deep_ocean_sand_bubbles.png',
                    animation = {
                        type = 'vertical_frames',
                        aspect_w = 16,
                        aspect_h = 16,
                        length = 1,
                    },
                    alpha_tween = {
                        1, 0.5,
                        style = 'fwd',
                        reps = 1
                    }
                },
                glow = 7
            }
        end

        minetest.add_particlespawner(particlespawner_def)
    end
})

-- Mineral Waters Water Geyser
minetest.register_abm({
    label = 'everness:water_geyser',
    nodenames = { 'everness:water_geyser' },
    interval = 16,
    chance = 16,
    catch_up = false,
    action = function(pos, node)
        minetest.swap_node(pos, { name = 'everness:water_geyser_active' })

        local meta = minetest.get_meta(pos)
        local partcile_time = math.random(5, 15)

        -- player
        for _, object in ipairs(minetest.get_objects_in_area(vector.new(pos.x - 0.5, pos.y - 0.5, pos.z - 0.5), vector.new(pos.x + 0.5, pos.y + 1, pos.z + 0.5))) do
            if object:is_player()
                and object:get_hp() > 0
            then
                object:add_velocity(vector.new(0, math.random(27, 32), 0))
            end
        end

        -- particles
        local particlespawner_def = {
            amount = 80,
            time = partcile_time,
            minpos = vector.new(pos.x, pos.y + 1.5, pos.z),
            maxpos = vector.new(pos.x, pos.y + 2, pos.z),
            minvel = vector.new(0, 13, 0),
            maxvel = vector.new(0, 15, 0),
            minacc = vector.new(0, -1, 1),
            maxacc = vector.new(0, -3, 2),
            minexptime = 3,
            maxexptime = 5,
            minsize = 5,
            maxsize = 7,
            texture = 'everness_water_geyser_particle.png',
            vertical = true,
            collisiondetection = true,
            collision_removal = true
        }

        if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then
            -- new syntax, above v5.6.0
            particlespawner_def = {
                amount = 80,
                time = partcile_time,
                size = {
                    min = 5,
                    max = 7,
                },
                exptime = {
                    min = 3,
                    max = 5
                },
                pos = {
                    min = vector.new(pos.x, pos.y + 1.5, pos.z),
                    max = vector.new(pos.x, pos.y + 2, pos.z)
                },
                vel = {
                    min = vector.new(0, 13, 0),
                    max = vector.new(0, 15, 0)
                },
                acc = {
                    min = vector.new(0, -1, 1),
                    max = vector.new(0, -3, 2)
                },
                texture = {
                    name = 'everness_water_geyser_particle.png',
                    scale_tween = {
                        5, 10,
                        style = 'fwd',
                        reps = 1
                    },
                    alpha_tween = {
                        1, 0,
                        style = 'fwd',
                        reps = 1
                    },
                    blend = 'alpha',
                },
                vertical = true,
                collisiondetection = true,
                collision_removal = true
            }
        end

        local particle_id = minetest.add_particlespawner(particlespawner_def)
        meta:set_int('particle_id', particle_id)

        minetest.sound_play({
            name = 'everness_water_geyser',
            gain = 1.5,
            pitch = math.random(10, 30) / 10
        }, {
            pos = pos
        })

        minetest.get_node_timer(pos):start(partcile_time)
    end
})

-- Generate bamboo tops after mineral waters biome generates decorations
minetest.register_lbm({
    -- Descriptive label for profiling purposes (optional).
    -- Definitions with identical labels will be listed as one.
    label = 'everness_mineral_waters_bamboo_large',

    -- Identifier of the LBM, should follow the modname:<whatever> convention
    name = 'everness:mineral_waters_bamboo_large',

    -- List of node names to trigger the LBM on.
    -- Names of non-registered nodes and groups (as group:groupname)
    -- will work as well.
    nodenames = { 'everness:bamboo_3' },

    -- Whether to run the LBM's action every time a block gets activated,
    -- and not only the first time the block gets activated after the LBM
    -- was introduced.
    run_at_every_load = true,

    -- Function triggered for each qualifying node.
    -- `dtime_s` is the in-game time (in seconds) elapsed since the block
    -- was last active
    action = function(pos, node, dtime_s)
        if minetest.get_node(vector.new(pos.x, pos.y + 1, pos.z)).name ~= 'air' then
            return
        end

        local node_below = minetest.get_node(vector.new(pos.x, pos.y - 1, pos.z))

        -- Get bamboo height
        local while_counter = 1
        local bamboo_height = 0
        local bamboo_below = node_below

        while bamboo_below.name == 'everness:bamboo_3' do
            bamboo_below = minetest.get_node(vector.new(pos.x, pos.y - while_counter, pos.z))
            bamboo_height = bamboo_height + 1
            while_counter = while_counter + 1
        end

        -- Add top bamboo nodes with leaves based on their generated height
        if bamboo_height > 4 then
            for i = 1, 3 do
                local pos_i = vector.new(pos.x, pos.y + i, pos.z)

                if minetest.get_node(pos_i).name == 'air' then
                    if i == 1 then
                        minetest.set_node(pos_i, {
                            name = 'everness:bamboo_4',
                            param2 = node_below.param2
                        })
                    else
                        minetest.set_node(pos_i, {
                            name = 'everness:bamboo_5',
                            param2 = node_below.param2
                        })
                    end
                end
            end
        else
            for i = 1, 2 do
                local pos_i = vector.new(pos.x, pos.y + i, pos.z)

                if minetest.get_node(pos_i).name == 'air' then
                    if i == 1 then
                        minetest.set_node(pos_i, {
                            name = 'everness:bamboo_4',
                            param2 = node_below.param2
                        })
                    else
                        minetest.set_node(pos_i, {
                            name = 'everness:bamboo_5',
                            param2 = node_below.param2
                        })
                    end
                end
            end
        end
    end
})

-- Activate timers on lotus flowers
minetest.register_lbm({
    -- Descriptive label for profiling purposes (optional).
    -- Definitions with identical labels will be listed as one.
    label = 'everness_lotus_flower_timers',

    -- Identifier of the LBM, should follow the modname:<whatever> convention
    name = 'everness:everness_lotus_flower_timers',

    -- List of node names to trigger the LBM on.
    -- Names of non-registered nodes and groups (as group:groupname)
    -- will work as well.
    nodenames = {
        'everness:lotus_flower_white',
        'everness:lotus_flower_purple',
        'everness:lotus_flower_pink'
    },

    -- Whether to run the LBM's action every time a block gets activated,
    -- and not only the first time the block gets activated after the LBM
    -- was introduced.
    run_at_every_load = true,

    -- Function triggered for each qualifying node.
    -- `dtime_s` is the in-game time (in seconds) elapsed since the block
    -- was last active
    action = function(pos, node, dtime_s)
        local timer = minetest.get_node_timer(pos)

        if not timer:is_started() then
            minetest.get_node_timer(pos):start(1)
        end
    end
})

-- Spread lotus flowers and leafs around them
minetest.register_abm({
    label = 'Lotus Flowers and Leafs spread',
    nodenames = {
        'everness:lotus_flower_white',
        'everness:lotus_flower_purple',
        'everness:lotus_flower_pink',
        'everness:lotus_leaf',
        'everness:lotus_leaf_2',
        'everness:lotus_leaf_3'
    },
    neighbors = {
        'everness:lotus_flower_white',
        'everness:lotus_flower_purple',
        'everness:lotus_flower_pink',
        'everness:lotus_leaf',
        'everness:lotus_leaf_2',
        'everness:lotus_leaf_3'
    },
    max_y = Everness.settings.biomes.everness_mineral_waters.y_max,
    min_y = Everness.settings.biomes.everness_mineral_waters.y_min,
    interval = 13,
    chance = 300,
    action = function(pos, node)
        local under = minetest.get_node(vector.new(pos.x, pos.y - 1, pos.z))
        local def = minetest.registered_nodes[under.name] or {}

        if def.liquidtype ~= 'source' and minetest.get_item_group(under.name, 'water') == 0 then
            return
        end

        local light = minetest.get_node_light(pos)

        if not light or light < 13 then
            return
        end

        local pos0 = vector.subtract(pos, 4)
        local pos1 = vector.add(pos, 4)
        local flower_node_names = {
            'everness:lotus_flower_white',
            'everness:lotus_flower_purple',
            'everness:lotus_flower_pink'
        }
        local leaf_node_names = {
            'everness:lotus_leaf',
            'everness:lotus_leaf_2',
            'everness:lotus_leaf_3'
        }
        local node_name = flower_node_names[math.random(1, #flower_node_names)]
        local found_flower_positions = minetest.find_nodes_in_area(pos0, pos1, flower_node_names)

        -- Testing shows that a threshold of 1 result in an appropriate maximum
        -- density of approximately 7 flowers per 9x9 area.
        if #found_flower_positions > 1 then
            -- Spread leafs
            local rand_flower_pos = found_flower_positions[math.random(1, #found_flower_positions)]
            pos0 = vector.subtract(rand_flower_pos, 4)
            pos1 = vector.add(rand_flower_pos, 4)
            local found_leaf_positions = minetest.find_nodes_in_area(pos0, pos1, leaf_node_names)

            if #found_leaf_positions > 25 then
                return
            end

            node_name = leaf_node_names[math.random(1, #leaf_node_names)]
        end

        local water_positions = minetest.find_nodes_in_area_under_air(pos0, pos1, 'group:water')

        table.shuffle(water_positions)
        -- Sort with the closest first
        table.sort(water_positions, function(a, b)
            return vector.distance(a, pos) < vector.distance(b, pos)
        end)

        local water_pos

        -- find water source since we are looking only for `group:water`
        for _, p in ipairs(water_positions) do
            local n = minetest.get_node(p)
            local d = minetest.registered_nodes[n.name] or {}

            if d.liquidtype == 'source' then
                water_pos = p
                break
            end
        end

        if not water_pos then
            return
        end

        local water_above = vector.new(water_pos.x, water_pos.y + 1, water_pos.z)
        light = minetest.get_node_light(water_above)

        if light and light >= 13 then
            minetest.set_node(water_above, { name = node_name })
        end
    end
})

-- Override lava cooling to include some variations of obsidian
minetest.register_on_mods_loaded(function()
    for _, abm in pairs(minetest.registered_abms) do
        if abm.label == 'Lava cooling' and abm.action ~= nil then
            local prev_cool_lava_action = abm.action

            abm.action = function(pos, node, dtime_s)
                Everness.cool_lava(pos, node, dtime_s, prev_cool_lava_action)
            end
        end
    end
end)

-- Calculates `everness:hammer_sharp` wear when crafting
minetest.register_craft_predict(function(itemstack, player, old_craft_grid, craft_inv)
    if itemstack and itemstack:get_name() == 'everness:hammer_sharp' then
        local stack_meta = itemstack:get_meta()
        local hammers = 0
        local wear_total = 0

        for k, stack in pairs(old_craft_grid) do
            if stack:get_name() == 'everness:hammer' then
                local meta = stack:get_meta()
                wear_total = wear_total + meta:get_int('everness_wear')
                hammers = hammers + 1
            end
        end

        local average_wear = wear_total / hammers

        stack_meta:set_int('everness_wear', math.ceil(average_wear))

        -- Draw wear bar texture overlay
        if average_wear > 0 then
            Everness.draw_wear_bar(itemstack, average_wear)
        end

        return itemstack
    end
end)

-- Calculates `everness:hammer_sharp` wear when crafting
minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv)
    if itemstack and itemstack:get_name() == 'everness:hammer_sharp' then
        local stack_meta = itemstack:get_meta()
        local hammers = 0
        local wear_total = 0

        for k, stack in pairs(old_craft_grid) do
            if stack:get_name() == 'everness:hammer' then
                local meta = stack:get_meta()
                wear_total = wear_total + meta:get_int('everness_wear')
                hammers = hammers + 1
            end
        end

        local average_wear = wear_total / hammers

        stack_meta:set_int('everness_wear', math.ceil(average_wear))

        -- Draw wear bar texture overlay
        if average_wear > 0 then
            Everness.draw_wear_bar(itemstack, average_wear)
        end

        return itemstack
    end
end)
