minetest-mod-prestibags/init.lua
2013-03-07 16:14:32 -08:00

426 lines
13 KiB
Lua

--- prestibags Minetest mod
--
-- @author prestidigitator
-- @copyright 2013, licensed under WTFPL
---- Configuration
--- Width and height of bag inventory (>0)
local BAG_WIDTH = 3
local BAG_HEIGHT = 3
--- 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[1,"..yImg..";1,1;prestibags_bag_inv.png]"..
"list[detached:prestibags:bags;"..self.id..";2,0;"..
BAG_WIDTH..","..BAG_HEIGHT..";]"..
"list[current_player;main;0,"..yPlay..";8,4;]"
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
})
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:getpos()
local node = minetest.env: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.env:find_node_near(pos, 1.0, "air")
if flamePos then
minetest.env:add_node(flamePos,
{ name = "fire:basic_flame" })
end
end
return self:burn()
end
if minetest.env: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.env: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.env: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.env:find_node_near(pos, 1.0, "air")
if flamePos then
minetest.env: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)
print("DEBUG - 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.env: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.env: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
})
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.env: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.env:get_node(pos)
nodeType = node and minetest.registered_nodes[node.name]
end
if not pos then pos = player:getpos() 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 =
{
{ "", "group:wool", "" },
{ "group:wool", "", "group:wool" },
{ "group:wool", "group:wool", "group:wool" },
}
})