minetest-mods/thirsty/functions.lua

584 lines
21 KiB
Lua
Raw Normal View History

2024-11-10 14:47:55 +01:00
------------------------------------------------------------
-- _____ _ _ _ --
-- |_ _| |_ (_)_ _ __| |_ _ _ --
-- | | | ' \| | '_(_-< _| || | --
-- |_| |_||_|_|_| /__/\__|\_, | --
-- |__/ --
------------------------------------------------------------
-- Thirsty mod [functions] --
------------------------------------------------------------
-- See init.lua for license --
------------------------------------------------------------
--------------------------
-- Tier 0 API Functions --
--------------------------
-- regen_from_node is a table defining, for each node type, the
-- amount of hydro per second a player drinks by standing in it.
-- Default is 0.5 of a hydration per second
function thirsty.register_hydrate_node(node_name,drink_cup,regen)
local drink_cup = drink_cup or true
local regen = regen or 0.5
thirsty.config.regen_from_node[node_name] = regen
thirsty.config.fountain_type[node_name] = "w"
if drink_cup then
thirsty.register_drinkable_node(node_name)
end
end
--------------------------
-- Tier 1 API Functions --
--------------------------
-- node_drinkable: which nodes can we drink from, given a
-- container (a cup, a bowl etc.)
function thirsty.register_drinkable_node(node_name,max_hydrate)
local max_hydrate = max_hydrate or nil
thirsty.config.node_drinkable[node_name] = true
if max_hydrate then
thirsty.config.drink_from_node[node_name] = max_hydrate
end
end
function thirsty.on_use( old_on_use )
return function(itemstack, player, pointed_thing)
local node = nil
if pointed_thing and pointed_thing.type == 'node' then
node = minetest.get_node(pointed_thing.under)
end
thirsty.drink_handler(player, itemstack, node)
-- call original on_use, if provided
if old_on_use ~= nil then
return old_on_use(itemstack, player, pointed_thing)
else
return itemstack
end
end
end
function thirsty.augment_item_for_drinking( itemname, level )
local new_definition = {}
local level = level or thirsty.config.start
-- we need to be able to point at the water
new_definition.liquids_pointable = true
-- call closure generator with original on_use handler
new_definition.on_use = thirsty.on_use(
minetest.registered_items[itemname].on_use
)
-- overwrite the node definition with almost the original
minetest.override_item(itemname, new_definition)
-- add configuration settings
thirsty.config.drink_from_container[itemname] = level
end
--------------------------
-- Tier 2 API Functions --
--------------------------
function thirsty.drink(player, value, max_hyd, empty_vessel)
local max_hyd = max_hyd or 20
local value = value or 2
local pmeta = player:get_meta()
local hydro = pmeta:get_float("thirsty_hydro")
-- test whether we're not *above* max_hyd;
-- this function should not remove any overhydration
if hydro < max_hyd then
hydro = math.min(hydro + value, max_hyd)
--print("Drinking by "..value.." to "..hydro)
pmeta:set_float("thirsty_hydro", hydro)
minetest.sound_play("thirsty_breviceps_drink-drinking-liquid", { to_player = player:get_player_name(), gain = 2.0, })
if empty_vessel then
player:get_inventory():add_item("main", empty_vessel.." 1")
end
return true
end
return false
end
function thirsty.register_canteen(item_name,hydrate_capacity,max_hydrate,on_use)
local on_use = on_use or true
local max_hydrate = max_hydrate or thirsty.config.start
thirsty.config.container_capacity[item_name] = hydrate_capacity
thirsty.config.drink_from_container[item_name] = max_hydrate
local def = table.copy(minetest.registered_items[item_name])
def.liquids_pointable = true
def.stack_max = 1
def.type = "tool"
if on_use then
def.on_use = thirsty.on_use(nil)
end
minetest.register_tool(":"..item_name, def)
end
function thirsty.register_canteen_complex(item_name,hydrate_capacity,max_hydrate,full_image)
local full_item_name = item_name.."_full"
local max_hydrate = max_hydrate or thirsty.config.start
-- register new full version of item into thirsty tables and as tool.
thirsty.config.container_capacity[full_item_name] = hydrate_capacity
thirsty.config.drink_from_container[full_item_name] = max_hydrate
local def_f = table.copy(minetest.registered_items[item_name])
def_f.inventory_image = full_image or def_f.inventory_image
def_f.wield_image = full_image or def_f.inventory_image
def_f.liquids_pointable = true
def_f.stack_max = 1
def_f.type = "tool"
def_f.description = "Full "..def_f.description
def_f.on_use = thirsty.on_use(nil)
def_f.thirsty_empty = item_name
minetest.register_tool(":"..full_item_name, def_f)
-- modify empty_vessel registration
local def_e = table.copy(minetest.registered_items[item_name])
def_e.on_use = thirsty.on_use_empty()
def_e.liquids_pointable = true
def_e.name = nil
def_e.type = nil
minetest.override_item(item_name, def_e)
end
-- note not offically exposed to API yet, use with caution may change
function thirsty.on_use_empty()
return function(itemstack, player, pointed_thing)
local node = nil
if pointed_thing and pointed_thing.type == 'node' then
node = minetest.get_node(pointed_thing.under)
end
if node and thirsty.config.node_drinkable[node.name] then
local new_stack = {name = itemstack:get_name().."_full", count=1, wear=1, metadata=""}
player:get_inventory():add_item("main", new_stack)
itemstack:take_item()
return itemstack
end
end
end
--------------------------
-- Tier 3 API Functions --
--------------------------
function thirsty.on_rightclick( old_on_rightclick )
return function(pos, node, player, itemstack, pointed_thing)
thirsty.drink_handler(player, itemstack, node)
-- call original on_rightclick, if provided
if old_on_rightclick ~= nil then
return old_on_rightclick(pos, node, player, itemstack, pointed_thing)
else
return itemstack
end
end
end
--------------------------
-- Tier 4 API Functions --
--------------------------
function thirsty.register_water_fountain(node_name)
thirsty.config.fountain_type[node_name] = "f"
end
--------------------------
-- Tier 5 API Functions --
--------------------------
function thirsty.register_amulet_extractor(item_name,value)
thirsty.config.extraction_for_item[item_name] = value
end
function thirsty.register_amulet_supplier(item_name,value)
thirsty.config.injection_for_item[item_name] = value
end
function thirsty.register_amulet_thirst(item_name,value)
thirsty.config.thirst_adjust_item[item_name] = value
end
function thirsty.get_thirst_factor(player)
local name = player:get_player_name()
local pmeta = player:get_meta()
local pl = minetest.deserialize(pmeta:get_string("thirsty_values"))
return pl.thirst_factor
end
function thirsty.set_thirst_factor(player, factor)
local name = player:get_player_name()
local pmeta = player:get_meta()
local pl = minetest.deserialize(pmeta:get_string("thirsty_values"))
pl.thirst_factor = factor
pmeta:set_string("thirsty_values",minetest.serialize(pl))
end
-------------------------
-- Other API Functions --
-------------------------
function thirsty.get_hydro(player)
local pmeta = player:get_meta()
local hydro = pmeta:get_float("thirsty_hydro")
return hydro
end
----------------------------
-- Mod Internal Functions --
----------------------------
local damage_enabled = minetest.settings:get_bool("enable_damage")
function thirsty.on_joinplayer(player)
local name = player:get_player_name()
local pmeta = player:get_meta()
-- setup initial thirst player meta value if not present
if pmeta:get_float("thirsty_hydro") == 0 then
pmeta:set_float("thirsty_hydro", thirsty.config.start)
end
-- default entry for joining players
if pmeta:get_string("thirsty_values") == "" then
local pos = player:get_pos()
pmeta:set_string("thirsty_values", minetest.serialize({
last_pos = math.floor(pos.x) .. ':' .. math.floor(pos.z),
time_in_pos = 0.0,
pending_dmg = 0.0,
thirst_factor = 1.0
}))
end
thirsty.hud_init(player)
end
function thirsty.on_dieplayer(player)
local name = player:get_player_name()
local pos = player:get_pos()
local pmeta = player:get_meta()
pmeta:set_float("thirsty_hydro", thirsty.config.start)
pmeta:set_string("thirsty_values", minetest.serialize({
last_pos = math.floor(pos.x) .. ':' .. math.floor(pos.z),
time_in_pos = 0.0,
pending_dmg = 0.0,
thirst_factor = 1.0
}))
end
function thirsty.main_loop(dtime)
-- get thirsty
thirsty.time_next_tick = thirsty.time_next_tick - dtime
while thirsty.time_next_tick < 0.0 do
-- time for thirst
thirsty.time_next_tick = thirsty.time_next_tick + thirsty.config.tick_time
for _,player in ipairs(minetest.get_connected_players()) do
-- dead players don't get thirsty, or full for that matter :-P
if player:get_hp() <= 0 then
break
end
local name = player:get_player_name()
local pos = player:get_pos()
local pmeta = player:get_meta()
local hydro = pmeta:get_float("thirsty_hydro")
local pl = minetest.deserialize(pmeta:get_string("thirsty_values"))
-- how long have we been standing "here"?
-- (the node coordinates in X and Z should be enough)
local pos_hash = math.floor(pos.x) .. ':' .. math.floor(pos.z)
if pl.last_pos == pos_hash then
pl.time_in_pos = pl.time_in_pos + thirsty.config.tick_time
else
-- you moved!
pl.last_pos = pos_hash
pl.time_in_pos = 0.0
end
local pl_standing = pl.time_in_pos > thirsty.config.stand_still_for_drink
local pl_afk = pl.time_in_pos > thirsty.config.stand_still_for_afk
--print("Standing: " .. (pl_standing and 'true' or 'false' ) .. ", AFK: " .. (pl_afk and 'true' or 'false'))
pos.y = pos.y + 0.1
local node = minetest.get_node(pos)
local drink_per_second = thirsty.config.regen_from_node[node.name] or 0
-- fountaining (uses pos, slight changes ok)
for k, fountain in pairs(thirsty.fountains) do
local dx = fountain.pos.x - pos.x
local dy = fountain.pos.y - pos.y
local dz = fountain.pos.z - pos.z
local dist2 = dx * dx + dy * dy + dz * dz
local fdist = fountain.level * thirsty.config.fountain_distance_per_level -- max 100 nodes radius
--print (string.format("Distance from %s (%d): %f out of %f", k, fountain.level, math.sqrt(dist2), fdist ))
if dist2 < fdist * fdist then
-- in range, drink as if standing (still) in water
drink_per_second = math.max(thirsty.config.regen_from_fountain or 0, drink_per_second)
pl_standing = true
break -- no need to check the other fountains
end
end
-- amulets
-- TODO: I *guess* we need to optimize this, but I haven't
-- measured it yet. No premature optimizations!
local pl_inv = player:get_inventory()
local extractor_max = 0.0
local injector_max = 0.0
local amulet_thirst = false
local container_not_full = nil
local container_not_empty = nil
local inv_main = player:get_inventory():get_list('main')
for i, itemstack in ipairs(inv_main) do
local name = itemstack:get_name()
-- Amulets Hydration/Moisture
local injector_this = thirsty.config.injection_for_item[name]
if injector_this and injector_this > injector_max then
injector_max = injector_this
end
local extractor_this = thirsty.config.extraction_for_item[name]
if extractor_this and extractor_this > extractor_max then
extractor_max = extractor_this
end
if thirsty.config.container_capacity[name] then
local wear = itemstack:get_wear()
-- can be both!
if wear == 0 or wear > 1 then
container_not_full = { i, itemstack }
end
if wear > 0 and wear < 65534 then
container_not_empty = { i, itemstack }
end
end
-- Amulets of Thirst
local is_thirst_amulet = thirsty.config.thirst_adjust_item[name]
if is_thirst_amulet then
amulet_thirst = true
pl.thirst_factor = thirsty.config.thirst_adjust_item[name]
end
end
if amulet_thirst ~= true and pl.thirst_factor ~= 1.0 then
pl.thirst_factor = 1.0
end
if extractor_max > 0.0 and container_not_full then
local i = container_not_full[1]
local itemstack = container_not_full[2]
local capacity = thirsty.config.container_capacity[itemstack:get_name()]
local wear = itemstack:get_wear()
if wear == 0 then wear = 65535.0 end
local drink = extractor_max * thirsty.config.tick_time
local drinkwear = drink / capacity * 65535.0
wear = wear - drinkwear
if wear < 1 then wear = 1 end
itemstack:set_wear(wear)
player:get_inventory():set_stack("main", i, itemstack)
end
if injector_max > 0.0 and container_not_empty then
local i = container_not_empty[1]
local itemstack = container_not_empty[2]
local capacity = thirsty.config.container_capacity[itemstack:get_name()]
local wear = itemstack:get_wear()
if wear == 0 then wear = 65535.0 end
local drink = injector_max * thirsty.config.tick_time
local drink_missing = 20 - hydro
drink = math.max(math.min(drink, drink_missing), 0)
local drinkwear = drink / capacity * 65535.0
wear = wear + drinkwear
if wear > 65534 then wear = 65534 end
itemstack:set_wear(wear)
thirsty.drink(player, drink, 20)
hydro = pmeta:get_float("thirsty_hydro")
player:get_inventory():set_stack("main", i, itemstack)
end
if drink_per_second > 0 and pl_standing then
-- Drinking from the ground won't give you more than max
thirsty.drink(player, (drink_per_second * thirsty.config.tick_time)*2, 20)
pl.time_in_pos = 0.0
--print("Raising hydration by "..(drink_per_second*thirsty.config.tick_time).." to "..PPA.get_value(player, 'thirsty_hydro'))
else
if not pl_afk then
-- only get thirsty if not AFK
local amount = thirsty.config.thirst_per_second * thirsty.config.tick_time * pl.thirst_factor
pmeta:set_float("thirsty_hydro",(hydro - amount))
hydro = pmeta:get_float("thirsty_hydro")
--print("Lowering hydration by "..amount.." to "..hydro)
end
end
-- should we only update the hud on an actual change?
-- minetest.debug(player:get_player_name().." "..hydro)
thirsty.hud_update(player, hydro)
-- damage, if enabled
if damage_enabled then
-- maybe not the best way to do this, but it does mean
-- we can do anything with one tick loop
if hydro <= 0.0 and not pl_afk then
pl.pending_dmg = pl.pending_dmg + thirsty.config.damage_per_second * thirsty.config.tick_time
--print("Pending damage at " .. pl.pending_dmg)
if pl.pending_dmg > 1.0 then
local dmg = math.floor(pl.pending_dmg)
pl.pending_dmg = pl.pending_dmg - dmg
player:set_hp( player:get_hp() - dmg )
end
else
-- forget any pending damage when not thirsty
pl.pending_dmg = 0.0
end
end
pmeta:set_string("thirsty_values",minetest.serialize(pl))
end -- for players
-- check fountains for expiration
for k, fountain in pairs(thirsty.fountains) do
fountain.time_until_check = fountain.time_until_check - thirsty.config.tick_time
if fountain.time_until_check <= 0 then
-- remove fountain, the abm will set it again
--print("Removing fountain at " .. k)
thirsty.fountains[k] = nil
end
end
end
end
function thirsty.drink_handler(player, itemstack, node)
local pmeta = player:get_meta()
local hydro = pmeta:get_float("thirsty_hydro")
local pl = minetest.deserialize(pmeta:get_string("thirsty_values"))
local old_hydro = hydro
-- selectors, always true, to make the following code easier
local item_name = itemstack and itemstack:get_name() or ':'
local node_name = node and node.name or ':'
if thirsty.config.node_drinkable[node_name] then
-- we found something to drink!
local cont_level = thirsty.config.drink_from_container[item_name] or 0
local node_level = thirsty.config.drink_from_node[node_name] or 0
-- drink until level
local level = math.max(cont_level, node_level)
--print("Drinking to level " .. level)
thirsty.drink(player, level, level)
-- fill container, if applicable
if thirsty.config.container_capacity[item_name] then
--print("Filling a " .. item_name .. " to " .. thirsty.config.container_capacity[item_name])
itemstack:set_wear(1) -- "looks full"
end
elseif thirsty.config.container_capacity[item_name] then
-- drinking from a container
if itemstack:get_wear() ~= 0 then
local capacity = thirsty.config.container_capacity[item_name]
local hydro_missing = 20 - hydro;
if hydro_missing > 0 then
local wear_missing = hydro_missing / capacity * 65535.0;
local wear = itemstack:get_wear()
local new_wear = math.ceil(math.max(wear + wear_missing, 1))
if (new_wear > 65534) then
wear_missing = 65534 - wear
new_wear = 65534
end
-- deal with full/empty vessels, when empty remove
-- tool version and supply empty name to thirsty.drink
-- so player recieves empty version back
local empty_vessel_name
if new_wear >= 65534 then
if minetest.registered_items[item_name].thirsty_empty then
itemstack:take_item(1)
empty_vessel_name = minetest.registered_items[item_name].thirsty_empty
else
itemstack:set_wear(new_wear)
end
-- end full/empty vessel code block
else
itemstack:set_wear(new_wear)
end
if wear_missing > 0 then -- rounding glitches?
thirsty.drink(player, wear_missing * capacity / 65535.0, 20, empty_vessel_name)
hydro = pmeta:get_float("thirsty_hydro")
end
end
end
end
-- update HUD if value changed
if hydro ~= old_hydro then
thirsty.hud_update(player, hydro)
end
end
function thirsty.fountain_abm(pos, node)
local fountain_count = 0
local water_count = 0
local total_count = 0
for y = 0, thirsty.config.fountain_height do
for x = -y, y do
for z = -y, y do
local n = minetest.get_node({
x = pos.x + x,
y = pos.y - y + 1, -- start one *above* the fountain
z = pos.z + z
})
if n then
--print(string.format("%s at %d:%d:%d", n.name, pos.x+x, pos.y-y+1, pos.z+z))
total_count = total_count + 1
local type = thirsty.config.fountain_type[n.name] or ''
if type == 'f' then
fountain_count = fountain_count + 1
elseif type == 'w' then
water_count = water_count + 1
end
end
end
end
end
local level = math.min(thirsty.config.fountain_max_level, math.min(fountain_count, water_count))
--print(string.format("Fountain (%d): %d + %d / %d", level, fountain_count, water_count, total_count))
thirsty.fountains[string.format("%d:%d:%d", pos.x, pos.y, pos.z)] = {
pos = { x=pos.x, y=pos.y, z=pos.z },
level = level,
-- time until check is 20 seconds, or twice the average
-- time until the abm ticks again. Should be enough.
time_until_check = 20,
}
end