----------- -- Setup -- ----------- -- Version number local version = "1.0" -- Translation wrapper for descriptive strings local S = minetest.get_translator(minetest.get_current_modname()) -- Explicit local references to globals we use local core, vector, minetest, mcl_mobs = core, vector, minetest, mcl_mobs -- Bitwise operators -- Normally we'd do this but mod security doesn't allow it. Good news -- is that mod setup already loads the bit library. -- local bit = require("bit") --------------------------- -- Configuration options -- --------------------------- -- TODO: Expose these as conf options? -- global object we can attach config stuff and API functions to vl_evil_piles = { -- Version number version = version, -- full piles spawn a monster once every this many seconds pile_spawn_timeout = 7, -- empty piles take this long to spawn one pile_slow_spawn_timeout = 52, -- if spawning fails, we retry quickly... pile_spawn_retry = 5, -- ...but retry slows down over time (we need to miss -- pile_spawn_timeout on the way up...) pile_spawn_retry_increment = 4, -- ... and eventually gets super slow pile_spawn_retry_max = 385, punch_speedup = 35, -- Piles spawn randomly with a small amount of fill (max is 63) place_level_min = 9, place_level_max = 17, -- Digging the node removes a random amount of fill levels, with the -- node being destroyed only when it reaches 0 dig_removes_min = 7, dig_removes_max = 11, -- Total entity count above which we'll skip spawning more max_mobs = 53, -- Total entity count of same kind above which we'll stop spawning more max_same_kind_entities = 8, -- Seconds to wait before picking up dropped item; same as in -- ENTITIES/mcl_item_entity pickup_buffer = 0.65, } -- Constant we use to mask off the liquid fill bits. Also the max fill -- level as a number. local fill_mask = 0x3f -- 63 in decimal ------------- -- Helpers -- ------------- -- Gets the fill level (0-63) from the first 6 bits of the param2 -- value from the given node table (must have param2 entry). function get_glasslike_liquid_fill_level(node_table) return bit.band(node_table.param2, fill_mask) end -- Uses swap_node to replace the fill level of the node at the given -- position. Level must be between 0 and 63 (inclusive) otherwise it -- will get wrapped into that range. We assume the node at the given -- position has a glasslike liquid fill level, if now we'll be -- shredding its param2 value. If node_table is not nil, it's used in -- place of getting the node table using the position; use that if you -- already have the node's data, otherwise just use 2 arguments -- (extras default to nil in Lua). Silently does nothing if applied to -- an unloaded position. function set_glasslike_liquid_fill_level(pos, level, node_table) -- Ensure new level is 0-63 local new_level = bit.band(level, fill_mask) -- Get node table if we weren't given one if node_table == nil then node_table = core.get_node_or_nil(pos) if node_table == nil then return -- give up if no table given and pos is not loaded end end -- Get other non-fill-level its from old param2 local old_param2 = node_table.param2 local rest = bit.band(old_param2, bit.bnot(fill_mask)) -- Combine old other bits with new fill bits local new_param2 = bit.bor(rest, new_level) -- Swap node to update param2 value without callbacks core.swap_node( pos, {name=node_table.name, param1=node_table.param1, param2=new_param2} ) end -- Returns the timeout in seconds corresponding to the given fill -- level. Fill levels are 0-63; timeouts range between local function timeout_for_fill(level) local fill_fraction = level / (fill_mask + 0.0) -- reset timeout to default on successful spawn return vl_evil_piles.pile_spawn_timeout + ( vl_evil_piles.pile_slow_spawn_timeout - vl_evil_piles.pile_spawn_timeout ) * (1 - fill_fraction) end ------------ -- Biomes -- ------------ local cold_biomes = { "ColdTaiga_underground", "IcePlains_underground", "IcePlainsSpikes_underground", "MegaTaiga_underground", "ColdTaiga", "IcePlainsSpikes", "IcePlains", "ExtremeHills+_snowtop", } local standard_biomes = { "FlowerForest_underground", "JungleEdge_underground", "Taiga_underground", "ExtremeHills+_underground", "JungleM_underground", "ExtremeHillsM_underground", "JungleEdgeM_underground", "MangroveSwamp_underground", "FlowerForest", "Swampland", "Taiga", "ExtremeHills", "ExtremeHillsM", "ExtremeHills+_snowtop", "Jungle", "Savanna", "BirchForest", "MegaSpruceTaiga", "MegaTaiga", "ExtremeHills+", "Forest", "Plains", "MushroomIsland", "SunflowerPlains", "RoofedForest", "JungleEdgeM", "JungleM", "BirchForestM", "MesaPlateauFM_grasstop", "MesaPlateauF", "MesaPlateauFM", "MesaPlateauF_grasstop", "JungleEdge", "SavannaM", "MangroveSwamp", "BambooJungle", "BambooJungleEdge", "BambooJungleEdgeM", "BambooJungleM", } local ocean_biomes = { "RoofedForest_ocean", "JungleEdgeM_ocean", "BirchForestM_ocean", "BirchForest_ocean", "IcePlains_deep_ocean", "Jungle_deep_ocean", "Savanna_ocean", "MesaPlateauF_ocean", "ExtremeHillsM_deep_ocean", "Savanna_deep_ocean", "SunflowerPlains_ocean", "Swampland_deep_ocean", "Swampland_ocean", "MegaSpruceTaiga_deep_ocean", "ExtremeHillsM_ocean", "JungleEdgeM_deep_ocean", "SunflowerPlains_deep_ocean", "BirchForest_deep_ocean", "IcePlainsSpikes_ocean", "Mesa_ocean", "StoneBeach_ocean", "Plains_deep_ocean", "JungleEdge_deep_ocean", "SavannaM_deep_ocean", "Desert_deep_ocean", "Mesa_deep_ocean", "ColdTaiga_deep_ocean", "Plains_ocean", "MesaPlateauFM_ocean", "Forest_deep_ocean", "JungleM_deep_ocean", "FlowerForest_deep_ocean", "MushroomIsland_ocean", "MegaTaiga_ocean", "StoneBeach_deep_ocean", "IcePlainsSpikes_deep_ocean", "ColdTaiga_ocean", "SavannaM_ocean", "MesaPlateauF_deep_ocean", "MesaBryce_deep_ocean", "ExtremeHills+_deep_ocean", "ExtremeHills_ocean", "MushroomIsland_deep_ocean", "Forest_ocean", "MegaTaiga_deep_ocean", "JungleEdge_ocean", "MesaBryce_ocean", "MegaSpruceTaiga_ocean", "ExtremeHills+_ocean", "Jungle_ocean", "RoofedForest_deep_ocean", "IcePlains_ocean", "FlowerForest_ocean", "ExtremeHills_deep_ocean", "MesaPlateauFM_deep_ocean", "Desert_ocean", "Taiga_ocean", "BirchForestM_deep_ocean", "Taiga_deep_ocean", "JungleM_ocean", "MangroveSwamp_ocean", "MangroveSwamp_deep_ocean", "FlowerForest_beach", "Forest_beach", "StoneBeach", "ColdTaiga_beach_water", "Taiga_beach", "Savanna_beach", "Plains_beach", "ExtremeHills_beach", "ColdTaiga_beach", "Swampland_shore", "MushroomIslandShore", "JungleM_shore", "Jungle_shore", "BambooJungleM_shore", "BambooJungle_shore", "MangroveSwamp_shore", } local desert_biomes = { "Mesa", "Desert", "MesaBryce", } local dimension_biomes = { "Nether", "BasaltDelta", "CrimsonForest", "WarpedForest", "SoulsandValley", "End", } ------------- -- Patches -- ------------- -- TODO: It looks like you *cannot* edit already-registered entity -- definitions... You can edit the Lua tables in the registry, but this -- does NOT affect the actual properties of the mob. -- Edits to registered skeleton/zombie mobs -- local skeleton_def = mcl_mobs.registered_mobs["mobs_mc:skeleton"] -- local stray_def = mcl_mobs.registered_mobs["mobs_mc:stray"] -- local zombie_def = mcl_mobs.registered_mobs["mobs_mc:zombie"] -- local baby_zombie_def = mcl_mobs.registered_mobs["mobs_mc:baby_zombie"] -- local husk_def = mcl_mobs.registered_mobs["mobs_mc:husk"] -- local baby_husk_def = mcl_mobs.registered_mobs["mobs_mc:baby_husk"] -- local villager_zombie_def = mcl_mobs.registered_mobs["mobs_mc:villager_zombie"] -- make husks drown in water (though they don't yet avoid it) -- if husk_def then husk_def.max_breath = 2 end -- if baby_husk_def then baby_husk_def.max_breath = 2 end -- Turn off sunlight damage for all undead, and drastically reduce -- their natural/random spawn rates -- for _, def in pairs({ -- skeleton_def, stray_def, zombie_def, baby_zombie_def, husk_def, -- baby_husk_def, villager_zombie_def -- }) do -- def.sunlight_damage = 0 -- def.ignited_by_sunlight = false -- -- TODO: Modify spawn chances instead if patch is accepted! -- -- see mcl_mobs/spawning.lua source -- def.spawn_check = function (pos, gotten_light, art_light, sky_light) -- -- Only 1/1000 chance to spawn normally -- local result = math.random(1000) == 1 -- return result -- end -- end ----------------------- -- Factory functions -- ----------------------- -- Creates an on_timer callback function to spawn a mob of the given -- name nearby to the block whose timer was triggered. local function make_pile_spawn_function(name) local function spawner(pos, elapsed, node, timeout) -- Did we spawn something? local spawned = false -- Can we spawn based on entity caps? local can_spawn = true -- Check spawn limits (global + same type) -- TODO: Ignore permanent mobs in spawn cap? local monsters_count = 0 local type_count = 0 for _, entity in pairs(minetest.luaentities) do if ( entity.is_mob and core.registered_entities[entity.name].type == "monster" ) then monsters_count = monsters_count + 1 end if entity.name == name then type_count = type_count + 1 end end if ( monsters_count >= vl_evil_piles.max_mobs or type_count >= vl_evil_piles.max_same_kind_entities ) then can_spawn = false end -- Search for place to put the mob in 5x5 surroundings if can_spawn then local where = vector.copy(pos) local ground, bot, top local g_name, b_name, t_name local g_solid, b_solid, t_solid for _, dz in ipairs({0, -1, 1, -2, 2}) do where.z = pos.z + dz for _, dx in ipairs({0, -1, 1, -2, 2}) do where.x = pos.x + dx local valid = false for _, dy in ipairs({2, 1, 0, -1, -2}) do where.y = pos.y + dy if not valid then ground = vector.offset(where, 0, -1, 0) bot = vector.copy(where) top = vector.offset(where, 0, 1, 0) -- get names for ground/bottom/top g_name = core.get_node(ground).name b_name = core.get_node(bot).name t_name = core.get_node(top).name if g_name and b_name and t_name then -- get solid group levels g_solid = minetest.get_item_group( g_name, "solid" ) b_solid = minetest.get_item_group( b_name, "solid" ) t_solid = minetest.get_item_group( t_name, "solid" ) valid = true else valid = false end else t_solid = b_solid t_name = b_name b_solid = g_solid b_name = g_name ground = vector.offset(where, 0, -1, 0) g_name = core.get_node(ground).name if g_name then g_solid = minetest.get_item_group( g_name, "solid" ) valid = true else valid = false end end -- only spawn if we have solid w/ 2x non-solid -- above it if ( valid and g_solid ~= 0 and (b_solid == 0 or b_solid == nil) and (t_solid == 0 or t_solid == nil) ) then -- We have to force the spawn because we -- disabled almost all spawns of these -- mobs above via a spawn check. TODO: -- force is unnecessary if we manipulated -- spawn rates above instead. local r = mcl_mobs.spawn( where, name, {force = true} ) if not r then local msg = string.format( "evil pile spawn of %s failed" .. " at pos %d,%d,%d", name, where.x, where.y, where.z ) core.log("verbose", msg) local ent = core.registered_entities[name] local m2 = string.format( "registered entity: %s", tostring(ent) ) local m3 = string.format( "it's a mob? %s", tostring(ent.is_mob) ) core.log("verbose", m2) core.log("verbose", m3) end spawned = true break end end if spawned then break end end if spawned then break end end -- end of z loop for search end -- skips entire search to here if cap was reached local t = core.get_node_timer(pos) local tt = t:get_timeout() -- Fill level from 0.0 to 1.0 (bits 0-5 of param2) if spawned then -- reset timeout to default on successful spawn t:start(timeout_for_fill(get_glasslike_liquid_fill_level(node))) else -- retry quickly on first failure if tt == vl_evil_piles.pile_spawn_timeout then t:start(vl_evil_piles.pile_spawn_retry) -- retry quickly else -- retry more and more slowly over time local nt = tt + vl_evil_piles.pile_spawn_retry_increment nt = math.min(nt, vl_evil_piles.pile_spawn_retry_max) t:start(nt) -- retry later end end return false end return spawner end -- Starts timer when pile is constructed local function construct_pile(pos) local h_node = core.get_node(pos) -- which node are we? local name = h_node.name local h_fill = get_glasslike_liquid_fill_level(h_node) if h_fill == 0 then -- Randomize the fill & set node which will put us back here -- with a non-zero fill h_fill = math.random( vl_evil_piles.place_level_min, vl_evil_piles.place_level_max ) set_glasslike_liquid_fill_level(pos, h_fill, h_node) -- set_glasslike_liquid_fill_level doesn't re-trigger -- on_construct so we keep going end -- Check for non-full piles below us and merge levels local below = vector.offset(pos, 0, -1, 0) local b_node = core.get_node(below) if b_node.name ~= name then -- not on top of another same-type pile local timer = core.get_node_timer(pos) timer:start(timeout_for_fill(h_fill)) else -- two piles on top of each other -- Get param2 for both nodes (bits 0-5 are level 0-63) local b_fill = get_glasslike_liquid_fill_level(b_node) local together = b_fill + h_fill if b_fill == fill_mask then -- Bottom node was full; don't consider merging -- Set up timer for this node local timer = core.get_node_timer(pos) timer:start(timeout_for_fill(h_fill)) elseif together <= fill_mask then -- Collapse into bottom node, leaving air behind core.remove_node(pos) set_glasslike_liquid_fill_level(below, together, b_node) -- Node timer should already be going for node below else -- Fill up bottom node and put rest in top node set_glasslike_liquid_fill_level(below, fill_mask, b_node) local leftover = together - fill_mask set_glasslike_liquid_fill_level(pos, leftover, h_node) -- Start timer for top node; should be going already below local timer = core.get_node_timer(pos) timer:start(timeout_for_fill(leftover)) end end end -- Speeds up timer every time pile is punched local function speedup_pile_timer(pos, node, puncher, pointed_thing) local meta = minetest.get_meta(pos) if puncher:is_player() then local t = core.get_node_timer(pos) local tt = t:get_timeout() -- speed up to original spawn timeout if longer tt = math.min(vl_evil_piles.pile_spawn_timeout, tt) local e = t:get_elapsed() local advanced = math.min( e + vl_evil_piles.punch_speedup, tt - 0.01 ) -- set new elapsed (and possibly timeout) t:set(tt, advanced) end end local function make_pile_add_function(node_name, material) local function add_to_pile(pos, node, clicker, itemstack, pointed_thing) -- if there's no item stack, don't do anything special if not itemstack then return end local old_level = get_glasslike_liquid_fill_level(node) -- if it's already full, don't do anything if old_level == fill_mask then return end local is_creative = false if clicker then local who = clicker:get_player_name() is_creative = minetest.is_creative_enabled(who) end local item_name = itemstack:get_name() -- full piles and individual material items add to the level if item_name == node_name or item_name == material then -- try to consume item from inventory (bounce if stack of 0) -- in creative mode don't take item if is_creative or itemstack:take_item(1) == 1 then -- default fill is same amount digging once removes local fill_by = math.random( vl_evil_piles.dig_removes_min, vl_evil_piles.dig_removes_max ) -- filling with a pile block fills more if item_name == node_name then fill_by = math.random( vl_evil_piles.place_level_min, vl_evil_piles.place_level_max ) end -- fill it up (overfull gets wasted local fill_to = math.min(fill_mask, old_level + fill_by) -- set new level set_glasslike_liquid_fill_level(pos, fill_to, node) end end -- Else do nothing special end return add_to_pile end local function make_pile_dig_function(material) local function dig_pile(pos, node, digger) local who = nil local is_creative = false if digger then who = digger:get_player_name() is_creative = minetest.is_creative_enabled(who) end local node = core.get_node(pos) -- which node are we? local fill = get_glasslike_liquid_fill_level(node) -- check privs if who ~= nil and core.is_protected(pos, who) then core.record_protection_violation(pos, who) return false -- can't dig protected node end -- wear item if tool and not in creative mode local dug_with = nil if digger and not is_creative then dug_with = digger:get_wielded_item() if dug_with then -- Use up one durability (should safely do nothing for -- non-tools because core API reference claims that -- add_wear does nothing for non-tools) mcl_util.use_item_durability(dug_with, 1) end end -- compute new fill level local removed = math.random( vl_evil_piles.dig_removes_min, vl_evil_piles.dig_removes_max ) local new_level = fill - removed; if is_creative then -- TODO: This after testing -- new_level = 0 -- always remove in one hit in creative mode end -- remove node and return true if new level <= 0, otherwise -- *don't* remove node, do drop one unit of material, reduce -- node's level, and return false if new_level <= 0 then -- remove the node core.remove_node(pos) -- TODO: Play some kind of distorted screaming sound -- here... -- Override drops if dug by a player; drops one of -- material by default local drops = { material } if digger then drops = minetest.get_node_drops(node, dug_with) end -- Handle drops via normal mechanism minetest.handle_node_drops(pos, drops, digger) return true else -- return false and spawn drop because levels remain -- TODO: Play some kind of distorted screaming sound -- here... -- Update to new level set_glasslike_liquid_fill_level(pos, new_level, node) -- fly straight up from top of block by default (might get -- stuck in block above, but this only happens when dug by -- non-player, which shouldn't happen much). local towards_digger = vector.new(0, 0.51, 0) if digger then -- Spawn item flying randomly out towards the player local digger_pos = digger:get_pos() towards_digger = vector.direction(pos, digger_pos) * 0.75 -- * 0.75 because it needs to clear block corner end local drops_at = pos + towards_digger -- generate drop ItemStack and get ObjectRef and lua entity local to_drop = ItemStack({name=material, count=1}) local obj = minetest.add_item(drops_at, to_drop) if obj then local ent = obj:get_luaentity() -- set random velocity towards_digger.x = towards_digger.x + math.random(-0.2, 0.2) towards_digger.y = towards_digger.y + math.random(-0.2, 0.2) towards_digger.z = towards_digger.z + math.random(-0.2, 0.2) obj:set_velocity(towards_digger) -- hopefully scale is fine? -- set age because mcl_item_entity does this in -- handle_node_drops; I'm not sure exactly what this does -- but presumably prevents immediate pickup? ent.age = vl_evil_piles.pickup_buffer end return false end end return dig_pile end function register_pile( name, desc, tiles, special_tiles, material, recipe, spawns, place_on, biomes, fill_ratio ) -- register block -- TODO: Use param2 to represent bones level w/ scaling on max spawns & -- interval local full_name = "vl_evil_piles:" .. name minetest.register_node( full_name, { description = desc, -- texture -- 'frame' texture first (1/16th edges) then 'shine' texture tiles = tiles, special_tiles = special_tiles, -- 'liquid' fill texture -- draws as 'liquid'-filled glass drawtype = "glasslike_framed", paramtype = "light", paramtype2 = "glasslikeliquidlevel", place_param2 = 0, -- empty; gets randomized -- hitbox node_box = { type = "fixed", fixed = { {-0.5, -0.5, -0,5}, {0.5, 0.5, 0.5}, } }, walkable = true, -- groups groups = { pickaxey = 2, destroy_by_lava_flow=1, dirtifier=1, enderman_takable=1, compostability=100, solid=1, opaque=1, falling_node=1 }, -- Takes a while to dig without a pickaxe _mcl_hardness = 1.5, -- Can't stack them stack_max = 1, -- Can be carved away by caves/structures is_ground_content = true, -- set up spawn timer when constructed on_construct = construct_pile, -- add items to the pile on_rightclick = make_pile_add_function(full_name, material), -- digging reduces level each time and only removes node -- when the level reaches 0 on_dig = make_pile_dig_function(material), -- If you punch it, it will speed up the spawn timer on_punch = speedup_pile_timer, -- spawns a skeleton when the timer goes off on_timer = make_pile_spawn_function(spawns), -- drops same item used to build it drop = material, -- drops itself when mined with silk-touch _mcl_silk_touch_drop = true, } ) --register recipe for block minetest.register_craft({ output = full_name .. " 1", recipe = recipe }) --register as decoration (adds to world gen) minetest.register_decoration({ deco_type = "simple", --what other blocks can it spawn on? place_on = place_on, biomes = biomes, sidelen = 32, -- Size of the square (X / Z) divisions of the mapchunk being -- generated. Determines the resolution of noise variation if -- used. If the chunk size is not evenly divisible by sidelen, -- sidelen is made equal to the chunk size. noise_params = { offset = 0, scale = fill_ratio, spread = {x = 1024, y = 1024, z = 1024}, seed = 43984, -- We assume world seed gets added automatically octaves = 3, -- 1024, 512, and 256 persistance = 0.5, -- factor for each octave lacunarity = 2.0, -- scaling factor between octaves flags = "absvalue" }, fill_ratio = fill_ratio, -- not used due to noise_params being set decoration = full_name, param2 = 1, -- lower limit for fill level param2_max = 55, -- upper limit for fill level (full is 63) }) end register_pile( -- ID (w/out mod prefix) "bone_pile", -- description S("Evil pile of bones from which skeletons arise."), -- frame + gloss {"bone_frame.png", "bone_mesh.png"}, -- fill {"bone_fill.png"}, -- material "mcl_mobitems:bone", -- recipe { {"", "mcl_mobitems:bone", "" }, {"mcl_mobitems:bone", "mcl_mobitems:bone", "mcl_mobitems:bone"}, {"mcl_mobitems:bone", "mcl_mobitems:bone", "mcl_mobitems:bone"}, }, -- spawns "mobs_mc:skeleton", -- place_on {"mcl_core:dirt_with_grass", "mcl_core:dirt", "mcl_core:stone"}, -- biomes standard_biomes, -- fill ratio 0.0002 ) register_pile( -- ID (w/out mod prefix) "icy_bone_pile", -- description S("Evil pile of icy bones from which strays arise."), -- frame + gloss {"icy_bone_frame.png", "icy_bone_mesh.png"}, -- fill {"icy_bone_fill.png"}, -- material "mcl_mobitems:bone", -- recipe { {"", "mcl_mobitems:bone", "" }, {"mcl_mobitems:bone", "mcl_core:ice", "mcl_mobitems:bone"}, {"mcl_mobitems:bone", "mcl_mobitems:bone", "mcl_mobitems:bone"}, }, -- spawns "mobs_mc:stray", -- place_on {"mcl_core:snow", "mcl_core:ice", "mcl_core:stone"}, -- biomes cold_biomes, -- fill ratio 0.0001 ) register_pile( -- ID (w/out mod prefix) "flesh_pile", -- description S("Evil pile of rotting flesh from which zombies arise."), -- frame + gloss {"flesh_frame.png", "flesh_mesh.png"}, -- fill {"flesh_fill.png"}, -- material "mcl_mobitems:rotten_flesh", -- recipe { {"", "mcl_mobitems:rotten_flesh", "" }, {"mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh"}, {"mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh"}, }, -- spawns "mobs_mc:zombie", -- place_on {"mcl_core:dirt_with_grass", "mcl_core:dirt", "mcl_core:stone"}, -- biomes standard_biomes, -- fill ratio 0.0002 ) register_pile( -- ID (w/out mod prefix) "dessicated_flesh_pile", -- description S("Evil pile of dessicated flesh from which husks arise."), -- frame + gloss {"dessicated_flesh_frame.png", "dessicated_flesh_mesh.png"}, -- fill {"dessicated_flesh_fill.png"}, -- material "mcl_mobitems:rotten_flesh", -- recipe { {"", "mcl_mobitems:rotten_flesh", "" }, {"mcl_mobitems:rotten_flesh", "mcl_core:sand", "mcl_mobitems:rotten_flesh"}, {"mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh", "mcl_mobitems:rotten_flesh"}, }, -- spawns "mobs_mc:husk", -- place_on {"mcl_core:sand", "mcl_core:sandstone", "mcl_core:sandstonesmooth", "mcl_core:sandstonecarved"}, -- biomes desert_biomes, -- fill ratio 0.0001 ) -- Since voxel manip won't start pile timers for us and -- loading/unloading might theoretically result in piles that don't -- have timers running, we register a slow ABM to restart pile timers. core.register_abm({ label = "evil piles timer starter", nodenames = { "vl_evil_piles:bone_pile", "vl_evil_piles:flesh_pile", "vl_evil_piles:dessicated_flesh_pile", "vl_evil_piles:icy_bones_pile" }, interval = 60.0, -- once per minute chance = 1, -- 100% chance of triggering action = function(pos, _1, _2, _3) -- start timer with default timeout if not started yet local t = core.get_node_timer(pos) if not t:is_started() then local n = core.get_node(pos) local duration = timeout_for_fill( get_glasslike_liquid_fill_level(n) ) t:start(duration) end end }) -- TODO: soaking_flesh_piles for underwater zombies? -- TODO: Add pile stacking within the block -- TODO: Use an LVM or ABM instead to constantly respawn these?