mirror of
https://github.com/prestidigitator/minetest-mod-prestibags.git
synced 2024-11-19 22:13:44 +01:00
426 lines
13 KiB
Lua
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" },
|
|
}
|
|
})
|