thirsty/init.lua

705 lines
24 KiB
Lua
Raw Normal View History

2015-04-07 13:01:43 -07:00
--[[
Thirsty mod [thirsty]
==========================
A mod that adds a "thirst" mechanic, similar to hunger.
Copyright (C) 2015 Ben Deutsch <ben@bendeutsch.de>
License
-------
2015-04-07 13:01:43 -07:00
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
Terminology: "Thirst" vs. "hydration"
-------------------------------------
"Thirst" is the absence of "hydration" (a term suggested by
everamzah on the Minetest forums, thanks!). The overall mechanic
is still called "thirst", but the visible bar is that of
"hydration", filled with "hydro points".
2015-04-07 13:01:43 -07:00
]]
-- the main module variable
thirsty = {
-- Configuration variables
config = {
-- see configuration.lua
},
-- the players' values
players = {
--[[
name = {
hydro = 20,
last_pos = '-10:3',
time_in_pos = 0.0,
pending_dmg = 0.0,
}
]]
},
-- water fountains
fountains = {
--[[
x:y:z = {
pos = { x=x, y=y, z=z },
level = 4,
time_until_check = 20,
-- something about times
}
]]
},
-- general settings
time_next_tick = 0.0,
2015-04-07 13:01:43 -07:00
}
dofile(minetest.get_modpath('thirsty')..'/configuration.lua')
thirsty.time_next_tick = thirsty.config.tick_time
dofile(minetest.get_modpath('thirsty')..'/hud.lua')
2015-04-07 13:01:43 -07:00
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
-- default entry for new players
if not thirsty.players[name] then
local pos = player:getpos()
thirsty.players[name] = {
hydro = 20,
last_pos = math.floor(pos.x) .. ':' .. math.floor(pos.z),
time_in_pos = 0.0,
pending_dmg = 0.0,
}
end
thirsty.hud_init(player)
2015-04-07 13:01:43 -07:00
end)
minetest.register_on_dieplayer(function(player)
local name = player:get_player_name()
-- fill after death
thirsty.players[name].hydro = 20;
end)
--[[
Main Loop (Tier 0)
]]
2015-04-07 13:01:43 -07:00
minetest.register_globalstep(function(dtime)
-- get thirsty
thirsty.time_next_tick = thirsty.time_next_tick - dtime
while thirsty.time_next_tick < 0.0 do
2015-04-07 13:01:43 -07:00
-- time for thirst
thirsty.time_next_tick = thirsty.time_next_tick + thirsty.config.tick_time
2015-04-07 13:01:43 -07:00
for _,player in ipairs(minetest.get_connected_players()) do
if player:get_hp() <= 0 then
-- dead players don't get thirsty, or full for that matter :-P
break
end
2015-04-07 13:01:43 -07:00
local name = player:get_player_name()
local pos = player:getpos()
local pl = thirsty.players[name]
-- 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
2015-04-13 12:55:49 -07:00
--print("Standing: " .. (pl_standing and 'true' or 'false' ) .. ", AFK: " .. (pl_afk and 'true' or 'false'))
pos.y = pos.y + 0.1
2015-04-07 13:01:43 -07:00
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 * 5 -- 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_node['default:water_source'] 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()
if pl_inv:contains_item('main', 'thirsty:injector') or pl_inv:contains_item('main', 'thirsty:extractor') then
local extractor_found = false
local injector_found = 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()
if name == 'thirsty:injector' then
injector_found = true
end
if name == 'thirsty:extractor' then
extractor_found = true
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
end
if extractor_found 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 = thirsty.config.extractor_speed * 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_found 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 = thirsty.config.injector_speed * thirsty.config.tick_time
local drink_missing = 20 - pl.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)
pl.hydro = pl.hydro + drink
if pl.hydro > 20 then pl.hydro = 20 end
player:get_inventory():set_stack("main", i, itemstack)
end
end -- if contains_item injector or extractor
if drink_per_second > 0 and pl_standing then
pl.hydro = pl.hydro + drink_per_second * thirsty.config.tick_time
-- Drinking from the ground won't give you more than max
if pl.hydro > 20 then pl.hydro = 20 end
--print("Raising hydration by "..(drink_per_second*thirsty.config.tick_time).." to "..pl.hydro)
else
if not pl_afk then
-- only get thirsty if not AFK
pl.hydro = pl.hydro - thirsty.config.thirst_per_second * thirsty.config.tick_time
if pl.hydro < 0 then pl.hydro = 0 end
--print("Lowering hydration by "..(thirsty.config.thirst_per_second*thirsty.config.tick_time).." to "..pl.hydro)
end
end
-- should we only update the hud on an actual change?
thirsty.hud_update(player, pl.hydro)
-- damage, if enabled
if minetest.setting_getbool("enable_damage") then
-- maybe not the best way to do this, but it does mean
-- we can do anything with one tick loop
if pl.hydro <= 0.0 and not pl_afk then
pl.pending_dmg = pl.pending_dmg + thirsty.config.damage_per_second * thirsty.config.tick_time
2015-04-13 12:55:49 -07:00
--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
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
2015-04-07 13:01:43 -07:00
end
2015-04-07 13:01:43 -07:00
end
end)
--[[
Stash: persist the hydration values in a file in the world directory.
If this is missing or corrupted, then no worries: nobody's thirsty ;-)
]]
function thirsty.read_stash()
local filename = minetest.get_worldpath() .. "/" .. thirsty.config.stash_filename
local file, err = io.open(filename, "r")
if not file then
-- no problem, it's just not there
-- TODO: or parse err?
return
end
thirsty.players = {}
for line in file:lines() do
if string.match(line, '^%-%-') then
-- comment, ignore
elseif string.match(line, '^P [%d.]+ [%d.]+ .+') then
-- player line
-- is matching again really the best solution?
local hydro, dmg, name = string.match(line, '^P ([%d.]+) ([%d.]+) (.+)')
thirsty.players[name] = {
hydro = tonumber(hydro),
last_pos = '0:0', -- not true, but no matter
time_in_pos = 0.0,
pending_dmg = tonumber(dmg),
}
end
end
file:close()
end
function thirsty.write_stash()
local filename = minetest.get_worldpath() .. "/" .. thirsty.config.stash_filename
local file, err = io.open(filename, "w")
if not file then
minetest.log("error", "Thirsty: could not write " .. thirsty.config.stash_filename .. ": " ..err)
return
end
file:write('-- Stash file for Minetest mod [thirsty] --\n')
-- write players:
-- P <hydro> <pending_dmg> <name>
file:write('-- Player format: "P <hydro> <pending damage> <name>"\n')
for name, data in pairs(thirsty.players) do
file:write("P " .. data.hydro .. " " .. data.pending_dmg .. " " .. name .. "\n")
end
file:close()
end
--[[
General handler
Most tools, nodes and craftitems use the same code, so here it is:
]]
function thirsty.drink_handler(player, itemstack, node)
local pl = thirsty.players[player:get_player_name()]
local old_hydro = pl.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)
if pl.hydro < level then
pl.hydro = level
end
-- 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 - pl.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
itemstack:set_wear(new_wear)
if wear_missing > 0 then -- rounding glitches?
pl.hydro = pl.hydro + (wear_missing * capacity / 65535.0)
end
end
end
end
-- update HUD if value changed
if pl.hydro ~= old_hydro then
thirsty.hud_update(player, pl.hydro)
end
end
--[[
Adapters for drink_handler to on_use and on_rightclick slots.
These close over the next handler to call in a chain, if desired.
]]
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.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
--[[
Drinking containers (Tier 1)
Defines a simple wooden bowl which can be used on water to fill
your hydration.
Optionally also augments the nodes from vessels to enable drinking
on use.
]]
function thirsty.augment_node_for_drinking( nodename, level )
2015-04-22 22:08:51 -07:00
local new_definition = {}
-- 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(
2015-04-22 22:08:51 -07:00
minetest.registered_nodes[nodename].on_use
)
-- overwrite the node definition with almost the original
minetest.override_item(nodename, new_definition)
-- add configuration settings
thirsty.config.drink_from_container[nodename] = level
2015-04-22 22:08:51 -07:00
end
if (minetest.get_modpath("vessels")) then
-- add "drinking" to vessels
thirsty.augment_node_for_drinking('vessels:drinking_glass', 22)
thirsty.augment_node_for_drinking('vessels:glass_bottle', 24)
thirsty.augment_node_for_drinking('vessels:steel_bottle', 26)
2015-04-22 22:08:51 -07:00
end
-- our own simple wooden bowl
minetest.register_craftitem('thirsty:wooden_bowl', {
description = "Wooden bowl",
inventory_image = "thirsty_bowl_16.png",
liquids_pointable = true,
on_use = thirsty.on_use(nil),
2015-04-22 22:08:51 -07:00
})
minetest.register_craft({
output = "thirsty:wooden_bowl",
recipe = {
{"group:wood", "", "group:wood"},
{"", "group:wood", ""}
}
})
--[[
Hydro containers (Tier 2)
Defines canteens (currently two types, with different capacities),
tools which store hydro. They use wear to show their content
level in their durability bar; they do not disappear when used up.
Wear corresponds to hydro level as follows:
- a wear of 0 shows no durability bar -> empty (initial state)
- a wear of 1 shows a full durability bar -> full
- a wear of 65535 shows an empty durability bar -> empty
]]
minetest.register_tool('thirsty:steel_canteen', {
description = 'Steel canteen',
inventory_image = "thirsty_steel_canteen_16.png",
liquids_pointable = true,
stack_max = 1,
on_use = thirsty.on_use(nil),
})
minetest.register_tool('thirsty:bronze_canteen', {
description = 'Bronze canteen',
inventory_image = "thirsty_bronze_canteen_16.png",
liquids_pointable = true,
stack_max = 1,
on_use = thirsty.on_use(nil),
})
minetest.register_craft({
output = "thirsty:steel_canteen",
recipe = {
{ "group:wood", ""},
{ "default:steel_ingot", "default:steel_ingot"},
{ "default:steel_ingot", "default:steel_ingot"}
}
})
minetest.register_craft({
output = "thirsty:bronze_canteen",
recipe = {
{ "group:wood", ""},
2015-04-24 12:54:38 -07:00
{ "default:bronze_ingot", "default:bronze_ingot"},
{ "default:bronze_ingot", "default:bronze_ingot"}
}
})
--[[
Tier 3
]]
minetest.register_node('thirsty:drinking_fountain', {
description = 'Drinking fountain',
drawtype = 'nodebox',
tiles = {
-- top, bottom, right, left, front, back
'thirsty_drinkfount_top.png',
'thirsty_drinkfount_bottom.png',
'thirsty_drinkfount_side.png',
'thirsty_drinkfount_side.png',
'thirsty_drinkfount_side.png',
'thirsty_drinkfount_side.png',
},
paramtype = 'light',
groups = { cracky = 2 },
node_box = {
type = "fixed",
fixed = {
{ -3/16, -8/16, -3/16, 3/16, 3/16, 3/16 },
{ -8/16, 3/16, -8/16, 8/16, 6/16, 8/16 },
{ -8/16, 6/16, -8/16, 8/16, 8/16, -6/16 },
{ -8/16, 6/16, 6/16, 8/16, 8/16, 8/16 },
{ -8/16, 6/16, -6/16, -6/16, 8/16, 6/16 },
{ 6/16, 6/16, -6/16, 8/16, 8/16, 6/16 },
},
},
selection_box = {
type = "regular",
},
collision_box = {
type = "regular",
},
on_rightclick = thirsty.on_rightclick(nil),
})
minetest.register_craft({
output = "thirsty:drinking_fountain",
recipe = {
{ "default:stone", "bucket:bucket_water", "default:stone"},
{ "", "default:stone", ""},
{ "", "default:stone", ""}
}
})
--[[
Tier 4+: the water fountains, plus extenders
]]
minetest.register_node('thirsty:water_fountain', {
description = 'Water fountain',
tiles = {
-- top, bottom, right, left, front, back
'thirsty_waterfountain_top.png',
'thirsty_waterfountain_top.png',
'thirsty_waterfountain_side.png',
'thirsty_waterfountain_side.png',
'thirsty_waterfountain_side.png',
'thirsty_waterfountain_side.png',
},
paramtype = 'light',
groups = { cracky = 2 },
})
minetest.register_node('thirsty:water_extender', {
description = 'Water fountain extender',
tiles = {
'thirsty_waterextender_top.png',
'thirsty_waterextender_top.png',
'thirsty_waterextender_side.png',
'thirsty_waterextender_side.png',
'thirsty_waterextender_side.png',
'thirsty_waterextender_side.png',
},
paramtype = 'light',
groups = { cracky = 2 },
})
2015-04-28 22:06:06 -07:00
minetest.register_craft({
output = "thirsty:water_fountain",
recipe = {
{ "default:copper_ingot", "bucket:bucket_water", "default:copper_ingot"},
{ "", "default:copper_ingot", ""},
{ "default:copper_ingot", "default:mese_crystal", "default:copper_ingot"}
}
})
minetest.register_craft({
output = "thirsty:water_extender",
recipe = {
{ "", "bucket:bucket_water", ""},
{ "", "default:copper_ingot", ""},
{ "default:copper_ingot", "default:mese_crystal", "default:copper_ingot"}
}
})
minetest.register_abm({
nodenames = {'thirsty:water_fountain'},
interval = 2,
chance = 5,
action = function(pos, node)
local fountain_count = 0
local water_count = 0
local total_count = 0
for y = 0, 4 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
if n.name == 'thirsty:water_fountain' or n.name == 'thirsty:water_extender' then
fountain_count = fountain_count + 1
elseif n.name == 'default:water_source' or n.name == 'default:water_flowing' then
water_count = water_count + 1
end
end
end
end
end
local level = math.min(20, 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
})
--[[
Tier 5
These amulets don't do much; the actual code is above, where
they are searched for in player's inventories
]]
minetest.register_craftitem('thirsty:injector', {
description = 'Water injector',
2015-05-06 22:02:14 -07:00
inventory_image = 'thirsty_injector.png',
})
minetest.register_craft({
output = "thirsty:injector",
recipe = {
{ "default:diamond", "default:mese_crystal", "default:diamond"},
{ "default:mese_crystal", "bucket:bucket_water", "default:mese_crystal"},
{ "default:diamond", "default:mese_crystal", "default:diamond"}
}
})
minetest.register_craftitem('thirsty:extractor', {
description = 'Water extractor',
2015-05-06 22:02:14 -07:00
inventory_image = 'thirsty_extractor.png',
})
minetest.register_craft({
output = "thirsty:extractor",
recipe = {
{ "default:mese_crystal", "default:diamond", "default:mese_crystal"},
{ "default:diamond", "bucket:bucket_water", "default:diamond"},
{ "default:mese_crystal", "default:diamond", "default:mese_crystal"}
}
})
-- read on startup
thirsty.read_stash()
-- write on shutdown
minetest.register_on_shutdown(thirsty.write_stash)