nc_skyrealm-cd2025/item_falling.lua
Aaron Suen 96ac76d8d8 Migrate to a combined return database
Items and nodes are listed equally in a single array.  This makes
it a lot easier to fairly select random things from the complete
set to return, instead of the de facto stratified random choice
with equal weighting between items and nodes regardless of how
many of each there is.

The stratified weighting wasn't a problem before, when our
expectation was that all items and all nodes would be returned
instantly.  Now that we're spacing things out, order matters more.
2024-10-10 11:30:12 -04:00

207 lines
5.6 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local ItemStack, math, minetest, nodecore, string, type, vector
= ItemStack, math, minetest, nodecore, string, type, vector
local math_random, string_format
= math.random, string.format
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
local api = _G[modname]
local modstore = api.store
local storekey = "falling"
local cache
do
local s = modstore:get_string(storekey)
cache = s and s ~= "" and minetest.deserialize(s) or {}
end
local function getfallingthings(ipos)
local poskey = minetest.pos_to_string(ipos)
local found = cache[poskey] or {list = {}}
-- Convert legacy separate item/node lists to combined list
if not found.list then found.list = {} end
if found.item then
local s = found.item
local l = found.list
for i = 1, #s do l[#l + 1] = {i = s[i]} end
found.item = nil
end
if found.node then
local s = found.node
local l = found.list
for i = 1, #s do l[#l + 1] = s[i] end
found.node = nil
end
return found, function()
cache[poskey] = found
return modstore:set_string(storekey, minetest.serialize(cache))
end
end
local function fallingfor(self)
local pos = self.object:get_pos()
if not (pos and api.in_sky_realm(pos)) then return end
local vel = self.object:get_velocity()
if (not vel) or vel.y > -30 then return end
local oy = pos.y
while true do
local node = minetest.get_node(pos)
if node.name == "ignore" then break end
if not api.air[node.name] then return end
pos.y = pos.y - 1
end
pos.y = oy
return api.island_near(pos) or api.island_near(pos, {x = 0, y = 1, z = 0})
end
local function itemeffects(pos, thing)
api.teleportsound(pos)
if type(thing) == "string" then
thing = ItemStack(thing):get_name()
thing = thing and minetest.registered_items[thing]
end
return api.particleburst(pos, vector.new(0.25, 0.25, 0.25), thing)
end
local function poof(self, def)
if not def then return self.object:remove() end
local pos = self.object:get_pos()
itemeffects(pos, def)
return self.object:remove()
end
local function itemshortdesc(stack)
stack = ItemStack(stack)
local copy = ItemStack(stack:get_name())
copy:set_count(stack:get_count())
return copy:to_string()
end
nodecore.register_item_entity_step(function(self)
local ipos = fallingfor(self)
if not ipos then return end
local pt, save = getfallingthings(ipos)
pt = pt.list
local stack = ItemStack(self.itemstring)
local iname = stack:get_name()
for i = 1, #pt do
local pts = ItemStack(pt[i])
stack = pts:add_item(stack)
pt[i] = {i = pts:to_string()}
if stack:is_empty() then break end
end
if not stack:is_empty() then
pt[#pt + 1] = {i = stack:to_string()}
end
save()
nodecore.log("action", string_format("skyrealm %s dropped item %q at %s total %d",
api.islandhash(ipos), itemshortdesc(stack),
minetest.pos_to_string(self.object:get_pos(), 0), #pt))
local def = minetest.registered_items[iname]
return poof(self, def)
end)
nodecore.register_falling_node_step(function(self)
local ipos = fallingfor(self)
if not ipos then return end
local pt, save = getfallingthings(ipos)
pt = pt.list
pt[#pt + 1] = {
n = self.node,
m = self.meta
}
save()
nodecore.log("action", string_format("skyrealm %s dropped node %q at %s total %d",
api.islandhash(ipos), self.node.name,
minetest.pos_to_string(self.object:get_pos(), 0), #pt))
local def = minetest.registered_items[self.node.name]
return poof(self, def)
end)
local function findspot_start(pos)
if nodecore.near_unloaded(pos, nil, 16) then return end
for rel in nodecore.settlescan() do
local p = vector.add(pos, rel)
if nodecore.buildable_to(p) then return p end
end
end
local function findspot_trace(pos)
pos = findspot_start(pos)
if not pos then return end
local tpos = vector.add(pos, {
x = math_random() * 10 - 5,
y = math_random() * 10 + 5,
z = math_random() * 10 - 5,
})
for hit in minetest.raycast(pos, tpos, false, false) do
if hit.above and nodecore.buildable_to(hit.above)
then return hit.above end
end
return tpos
end
local function findspot(pos)
pos = findspot_trace(pos)
if not pos then return end
local below = vector.offset(pos, 0, -1, 0)
if not nodecore.buildable_to(below) then return pos end
for _ = 1, 16 do
pos.y = pos.y - 1
below.y = below.y - 1
if not nodecore.buildable_to(below) then return pos end
end
end
local function returncore(pos, ipos, list)
if #list < 1 then return end
pos = findspot(pos)
if not pos then return end
local idx = math_random(1, #list)
local pick = list[idx]
if not pick then return end
if idx ~= #list then list[idx] = list[#list] end
list[#list] = nil
itemeffects(pos, pick.i or pick.n.name)
nodecore.log("action", string_format(
"skyrealm %s %s arrived at %s left %d",
api.islandhash(ipos), pick.i
and string_format("item %q", pick.i)
or string_format("node %q", pick.n.name),
minetest.pos_to_string(pos), #list))
if pick.i then
nodecore.item_eject(pos, pick.i)
else
nodecore.set_loud(pos, pick.n)
minetest.get_meta(pos):from_table(pick.m)
end
nodecore.fallcheck(pos)
return true
end
local pending = {}
local function return_falling_delayed(key, pos, ipos)
local data, save = getfallingthings(ipos)
if not returncore(pos, ipos, data.list) then
pending[key] = nil
else
save()
pending[key] = minetest.after(math_random() / 4,
return_falling_delayed, key, pos, ipos)
end
end
function api.return_falling(pos, ipos)
local key = minetest.hash_node_position(vector.round(pos))
pending[key] = pending[key] or minetest.after(0,
return_falling_delayed, key, pos, ipos)
end