--- prestibags Minetest mod -- -- @author prestidigitator -- @copyright 2013, licensed under WTFPL ---- Configuration --- Width and height of bag inventory (>0) local BAG_WIDTH = 2 local BAG_HEIGHT = 1 --- Sound played when placing/dropping a bag on the ground local DROP_BAG_SOUND = "prestibags_drop_bag" local DROP_BAG_SOUND_GAIN = 1.0 local DROP_BAG_SOUND_DIST = 5.0 --- Sound played when opening a bag's inventory local OPEN_BAG_SOUND = "prestibags_rustle_bag" local OPEN_BAG_SOUND_GAIN = 1.0 local OPEN_BAG_SOUND_DIST = 5.0 --- HP of undamaged bag (integer >0). local BAG_MAX_HP = 4 --- How often the inventories of destroyed bags are checked and cleaned up -- (>0.0). local CLEANUP_PERIOD__S = 10.0 --- How often environmental effects like burning are checked (>0.0). local ENV_CHECK_PERIOD__S = 0.5 --- Max distance an igniter node can be and still ignite/burn the bag (>=1). local MAX_IGNITE_DIST = 4.0 --- Probability (0.0 <= p <= 1.0) bag will be damaged for each igniter touching -- it (increases if igniter's max range is greater than current distance). -- Always damaged if in lava. local BURN_DAMAGE_PROB = 0.25 --- Probability (0.0 <= p <= 1.0) bag will ignite and spawn some flames on or -- touching it for each igniter within igniter range (increases if -- igniter's max range is greater than current distance). Alawys ignites -- if in lava. Ignition is ignored if "fire:basic_flame" is not available. local IGNITE_PROB = 0.25 --- Amount of damage bag takes each time it is burned. Note that a bag can be -- burned at most once each update cycle, so this is the MAXIMUM damage -- taken by burning each ENV_CHECK_PERIOD__S period. local BURN_DAMAGE__HP = 1 ---- end of configuration local EPSILON = 0.001 -- "close enough" local function serializeContents(contents) if not contents then return "" end local tabs = {} for i, stack in ipairs(contents) do tabs[i] = stack and stack:to_table() or "" end return minetest.serialize(tabs) end local function deserializeContents(data) if not data or data == "" then return nil end local tabs = minetest.deserialize(data) if not tabs or type(tabs) ~= "table" then return nil end local contents = {} for i, tab in ipairs(tabs) do contents[i] = ItemStack(tab) end return contents end -- weak references to keep track of what detached inventory lists to remove local idSet = {} local idToWeakEntityMap = {} setmetatable(idToWeakEntityMap, { __mode = "v" }) local entityInv local function cleanInventory() for id, dummy in pairs(idSet) do if not idToWeakEntityMap[id] then entityInv:set_size(id, 0) idSet[id] = nil end end minetest.after(CLEANUP_PERIOD__S, cleanInventory) end minetest.after(CLEANUP_PERIOD__S, cleanInventory) entityInv = minetest.create_detached_inventory( "prestibags:bags", { allow_move = function(inv, fromList, fromIndex, toList, toIndex, count, player) return idToWeakEntityMap[fromList] and idToWeakEntityMap[toList] and count or 0 end, allow_put = function(inv, toList, toIndex, stack, player) return idToWeakEntityMap[toList] and stack:get_count() or 0 end, allow_take = function(inv, fromList, fromIndex, stack, player) return idToWeakEntityMap[fromList] and stack:get_count() or 0 end, on_move = function(inv, fromList, fromIndex, toList, toIndex, count, player) local fromEntity = idToWeakEntityMap[fromList] local toEntity = idToWeakEntityMap[toList] local fromStack = fromEntity.contents[fromIndex] local toStack = toEntity.contents[toIndex] local moved = fromStack:take_item(count) toStack:add_item(moved) end, on_put = function(inv, toList, toIndex, stack, player) local toEntity = idToWeakEntityMap[toList] local toStack = toEntity.contents[toIndex] toStack:add_item(stack) end, on_take = function(inv, fromList, fromIndex, stack, player) local fromEntity = idToWeakEntityMap[fromList] local fromStack = fromEntity.contents[fromIndex] fromStack:take_item(stack:get_count()) end }) local function bag_envUpdate(self, dt) end minetest.register_entity( "prestibags:bag_entity", { initial_properties = { hp_max = BAG_MAX_HP, physical = false, collisionbox = { -0.44, -0.5, -0.425, 0.44, 0.35, 0.425 }, visual = "mesh", visual_size = { x = 1, y = 1 }, mesh = "prestibags_bag.obj", textures = { "prestibags_bag.png" } }, on_activate = function(self, staticData, dt) local id repeat id = "bag"..(math.random(0, 2^15-1)*2^15 + math.random(0, 2^15-1)) until not idSet[id] idSet[id] = id idToWeakEntityMap[id] = self self.id = id self.object:set_armor_groups({ punch_operable = 1, flammable = 1 }) local contents = deserializeContents(staticData) if not contents then contents = {} for i = 1, BAG_WIDTH*BAG_HEIGHT do contents[#contents+1] = ItemStack(nil) end end self.contents = contents self.timer = ENV_CHECK_PERIOD__S end, get_staticdata = function(self) return serializeContents(self.contents) end, on_punch = function(self, hitterObj, timeSinceLastPunch, toolCaps, dir) local playerName = hitterObj:get_player_name() local playerInv = hitterObj:get_inventory() if not playerName or not playerInv then return end local contentData = serializeContents(self.contents) local hp = self.object:get_hp() local newItem = ItemStack({ name = "prestibags:bag", metadata = contentData, wear = (2^16) * (BAG_MAX_HP - hp) / BAG_MAX_HP }) if not playerInv:room_for_item("main", newItem) then return end self:remove() playerInv:add_item("main", newItem) end, on_rightclick = function(self, player) local invLoc = "detached:"..self.id local w = math.max(8, 2 + BAG_WIDTH) local h = math.max(5 + BAG_HEIGHT) local yImg = math.floor(BAG_HEIGHT/2) local yPlay = BAG_HEIGHT + 1 if not self.contents or #self.contents <= 0 then return end entityInv:set_size(self.id, #self.contents) for i, stack in ipairs(self.contents) do entityInv:set_stack(self.id, i, stack) end local formspec = "size["..w..","..h.."]".. "image[2,"..yImg..";1,1;prestibags_bag_inv.png]".. "list[detached:prestibags:bags;"..self.id..";3,0;".. BAG_WIDTH..","..BAG_HEIGHT..";]".. "list[current_player;main;0,"..yPlay..";8,4;]".. "listring[]" minetest.show_formspec( player:get_player_name(), "prestibags:bag", formspec) minetest.sound_play( OPEN_BAG_SOUND, { object = self.object, gain = OPEN_BAG_SOUND_GAIN, max_hear_distance = OPEN_BAG_SOUND_DIST, loop = false }, true) end, on_step = function(self, dt) self.timer = self.timer - dt if self.timer > 0.0 then return end self.timer = ENV_CHECK_PERIOD__S local haveFlame = minetest.registered_nodes["fire:basic_flame"] local pos = self.object:get_pos() local node = minetest.get_node(pos) local nodeType = node and minetest.registered_nodes[node.name] if nodeType and nodeType.walkable and not nodeType.buildable_to then return self:remove() end if minetest.get_item_group(node.name, "lava") > 0 then if haveFlame then local flamePos = minetest.find_node_near(pos, 1.0, "air") if flamePos then minetest.add_node(flamePos, { name = "fire:basic_flame" }) end end return self:burn() end if minetest.find_node_near(pos, 1.0, "group:puts_out_fire") then return end local minPos = { x = pos.x - MAX_IGNITE_DIST, y = pos.y - MAX_IGNITE_DIST, z = pos.z - MAX_IGNITE_DIST } local maxPos = { x = pos.x + MAX_IGNITE_DIST, y = pos.y + MAX_IGNITE_DIST, z = pos.z + MAX_IGNITE_DIST } local wasIgnited = false local burnLevels = 0.0 local igniterPosList = minetest.find_nodes_in_area(minPos, maxPos, "group:igniter") for i, igniterPos in ipairs(igniterPosList) do local distSq = (igniterPos.x - pos.x)^2 + (igniterPos.y - pos.y)^2 + (igniterPos.z - pos.z)^2 if distSq <= MAX_IGNITE_DIST^2 + EPSILON then local igniterNode = minetest.get_node(igniterPos) local igniterLevel = minetest.get_item_group(igniterNode.name, "igniter") - math.max(1.0, math.sqrt(distSq) - EPSILON) if igniterLevel >= 0.0 then if distSq <= 1.0 then wasIgnited = true end burnLevels = burnLevels + 1.0 + igniterLevel end end end if burnLevels >= 1.0 then if haveFlame and (not wasIgnited) and math.random() >= (1.0 - IGNITE_PROB)^burnLevels then local flamePos = (node.name == "air") and pos or minetest.find_node_near(pos, 1.0, "air") if flamePos then minetest.add_node(flamePos, { name = "fire:basic_flame" }) end end if math.random() >= (1.0 - BURN_DAMAGE_PROB)^burnLevels then self:burn() end end end, remove = function(self) entityInv:set_size(self.id, 0) idSet[self.id] = nil self.object:remove() end, burn = function(self) local hp = self.object:get_hp() - BURN_DAMAGE__HP self.object:set_hp(hp) minetest.log("verbose", "bag HP = "..hp) if hp <= 0 then return self:remove() end end }) local function rezEntity(stack, pos, player) local x = pos.x local y = math.floor(pos.y) local z = pos.z while true do local node = minetest.get_node({ x = x, y = y-1, z = z}) local nodeType = node and minetest.registered_nodes[node.name] if not nodeType or nodeType.walkable then break end y = y - 1 end local obj = minetest.add_entity(pos, "prestibags:bag_entity") if not obj then return stack end local contentData = stack:get_metadata() local contents = deserializeContents(contentData) if contents then obj:get_luaentity().contents = contents end obj:set_hp(BAG_MAX_HP - BAG_MAX_HP * stack:get_wear() / 2^16) minetest.sound_play( DROP_BAG_SOUND, { object = obj, gain = DROP_BAG_SOUND_GAIN, max_hear_distance = DROP_BAG_SOUND_DIST, loop = false }, true) return ItemStack(nil) end -- DEBUG minetest.register_craftitem( minetest.register_tool( "prestibags:bag", { description = "Bag of Stuff", groups = { bag = BAG_WIDTH*BAG_HEIGHT, flammable = 1 }, inventory_image = "prestibags_bag_inv.png", wield_image = "prestibags_bag_wield.png", stack_max = 1, on_place = function(stack, player, pointedThing) local pos = pointedThing and pointedThing.under local node = pos and minetest.get_node(pos) local nodeType = node and minetest.registered_nodes[node.name] if not nodeType or not nodeType.buildable_to then pos = pointedThing and pointedThing.above node = pos and minetest.get_node(pos) nodeType = node and minetest.registered_nodes[node.name] end if not pos then pos = player:get_pos() end return rezEntity(stack, pos, player) end, on_drop = function(stack, player, pos) return rezEntity(stack, pos, player) end -- Eventually add on_use(stack, player, pointedThing) which actually -- opens the bag from player inventory; trick is, has to track whether -- bag is still in inventory OR replace "player inventory" with a -- detached proxy that doesn't allow the bag's stack to be changed -- while open! }) minetest.register_craft( { output = "prestibags:bag", recipe = { { "", "farming:string", "" }, { "farming:string", "", "farming:string" }, { "farming:string", "farming:string", "farming:string" }, } })