code:
# radiant_damage

This mod allows registered nodes to damage a player if the player is simply near them, rather than having to be immersed in them directly. For example, in real life simply being close to lava would burn you. Or perhaps being near a highly radioactive material would be damaging.

This mod comes with a set of predefined radiant damage types, all of which can be enabled and disabled independently, and allows other mods to make use of its system to register their own.

## Configurable Presets

**Important note:** none of these predefined radiant damage types are enabled by default. This is because one of this mod's intended uses is as a library for other mods to use to enable their own radiant damage types. There is no way to de-register a globalstep callback (the mechanism used by this mod to deliver damage) once it has been registered, so to keep the mod maximally flexible nothing is registered by default.

Set one or more of the following types to enabled if you want this mod to have an effect out-of-the-box.

The following settings exist for predefined radiant damage types:

    radiant_damage_enable_heat_damage (Enable radiant heat damage) bool false
    radiant_damage_lava_damage (Damage dealt per second when standing directly adjacent to one lava node) int 8
    radiant_damage_fire_damage (Damage dealt per second when standing directly adjacent to one fire node) int 2

    radiant_damage_enable_mese_damage (Enable mese ore radiation damage) bool false
    radiant_damage_mese_interval (Number of seconds between mese radiation damage checks) int 5
    radiant_damage_mese_damage (Damage dealt per second when standing directly adjacent to one mese ore node) int 2

Mese radiation is attenuated by a factor of 0.9 when passing through most materials, by 0.5 when passing through anything with group:stone, by 0.1 when passing through anything with group:mese_radiation_shield (all default metal blocks are given this group), and is _amplified_ by a factor of 4 when passing through nodes with group:mese_radiation_amplifier (default coal and diamond blocks). Note that these fine-grained attenuation factors only work in Minetest 0.5 and higher, for 0.4.16 any non-air node will block all damage.

## API

### Registering a damage type

Call:

```
	radiant_damage.register_radiant_damage(damage_name, damage_def)
```

where damage_name is a string used to identify this damage type and damage_def is a table such as:

```
{
	interval = 1, -- number of seconds between each damage check. Defaults to 1 when undefined.
	range = 3, -- range of the damage. Can be omitted if inverse_square_falloff is true,
		-- in that case it defaults to the range at which 0.125 points of damage is done
		-- by the most damaging emitter node type.
	emitted_by = {}, -- nodes that emit this damage. At least one emission node type
		-- and damage value pair is required.
	attenuated_by = {} -- This allows certain intervening node types to modify the damage
		-- that radiates through it. This parameter is optional.
		-- Note: Only works in Minetest version 0.5 and above.
	default_attenuation = 1, -- the amount the damage is multiplied by when passing
		-- through any other non-air nodes. Defaults to 0 when undefined. Note that
		-- in versions before Minetest 0.5 any value other than 1 will result in total
		-- occlusion (ie, any non-air node will block all damage)
	inverse_square_falloff = true, -- if true, damage falls off with the inverse square
		-- of the distance. If false, damage is constant within the range. Defaults to
		-- true when undefined.
	above_only = false, -- if true, damage only propagates directly upward. Useful for
		-- when you want to damage players only when they stand on the node.
		-- Defaults to false when undefined.
	on_damage = function(player_object, damage_value, pos), -- An optional callback to allow mods
		-- to do custom behaviour. If this is set to non-nil then the default damage will
		-- *not* be done to the player, it's up to the callback to handle that. If it's left
		-- undefined then damage_value is dealt to the player.
}
```

emitted_by has the following format:
```
	{["default:stone_with_mese"] = 2, ["default:mese"] = 9}
```
where the value associated with each entry is the amount of damage dealt. Groups are permitted. Note that negative damage represents "healing" radiation.

attenuated_by has the following similar format:

```
	{["group:stone"] = 0.25, ["default:steelblock"] = 0}
```

where the value is a multiplier that is applied to the damage passing through it. Groups are permitted. Note that you can use values greater than one to make a node type magnify damage instead of attenuating it.

### Updating/overriding a registered damage type

To modify or add new parameters to an existing already-registered damage type use the following function:

```
	radiant_damage.override_radiant_damage(damage_name, damage_def)
```

Where damage_def is a table as above but which only includes the new information. For example, a mod could add a new type of mese radiation emitter with the following:

```
	radiant_damage.override_radiant_damage("mese", {
		emitted_by = {
			["dfcaverns:glow_mese"] = radiant_damage.config.mese_damage * 12,
		},
	})
```

To remove an emission source set its emitted damage to 0.

To remove an attenuation node type, set its attenuation factor to equal the default attenuation factor.

If you wish to "disable" a registered damage type, use this override function to set its range to 1 and its interval to an enormous value (millions of seconds) to neutralize the damage type's global callback most efficiently.

## Further reading

These wiki pages may be of interest regarding the principles behind some of this mods' functions:

* [Inverse-square law](https://en.wikipedia.org/wiki/Inverse-square_law)
* [Half-value layer](https://en.wikipedia.org/wiki/Half-value_layer)


code:
local CONFIG_FILE_PREFIX = "radiant_damage_"

radiant_damage.config = {}

local print_settingtypes = false

local function setting(stype, name, default, description)
	local value
	if stype == "bool" then
		value = minetest.settings:get_bool(CONFIG_FILE_PREFIX..name, default)
	elseif stype == "string" then
		value = minetest.settings:get(CONFIG_FILE_PREFIX..name)
	elseif stype == "int" or stype == "float" then
		value = tonumber(minetest.settings:get(CONFIG_FILE_PREFIX..name))
	end
	if value == nil then
		value = default
	end
	radiant_damage.config[name] = value

	if print_settingtypes then
		minetest.debug(CONFIG_FILE_PREFIX..name.." ("..description..") "..stype.." "..tostring(default))
	end
end

setting("bool", "enable_heat_damage", false, "Enable radiant lava damage")
setting("int", "lava_damage", 8, "Damage dealt per second when standing directly adjacent to one lava node")
setting("int", "fire_damage", 2, "Damage dealt per second when standing directly adjacent to one fire node")

setting("bool", "enable_mese_damage", false, "Enable mese ore radiation damage")
setting("int", "mese_interval", 5, "Number of seconds between mese radiation damage checks")
setting("int", "mese_damage", 2, "Damage dealt per interval when standing directly adjacent to one mese ore node")

repo_name: korkut/MeseCraft

license: mit


code:
radiant_damage = {} --create a container for functions and constants

local modpath = minetest.get_modpath(minetest.get_current_modname())

dofile(modpath.."/config.lua")

-- damage_def:
--{
--	interval = 1, -- number of seconds between each damage check
--	range = 3, -- range of the damage. Can be omitted if inverse_square_falloff is true, in that case it defaults to the range at which 1 point of damage is done by the most damaging emitter node type.
--	emitted_by = {}, -- nodes that emit this damage. At least one is required.
--	attenuated_by = {} -- This allows certain intervening node types to modify the damage that radiates through it. Note: Only works in Minetest version 0.5 and above.
--	default_attenuation = 1, -- the amount the damage is multiplied by when passing through any other non-air nodes. Note that in versions before Minetest 0.5 any value other than 1 will result in total occlusion (ie, any non-air node will block all damage)
--	inverse_square_falloff = true, -- if true, damage falls off with the inverse square of the distance. If false, damage is constant within the range.
--	above_only = false, -- if true, damage only propagates directly upward. Useful for when you want to damage players that stand on the node.
--	on_damage = function(player_object, damage_value, pos) -- An optional callback to allow mods to do custom behaviour. If this is set to non-nil then the default damage will *not* be done to the player, it's up to the callback to handle that.
--}

-- emitted_by has the following format:
-- {["default:stone_with_mese"] = 2, ["default:mese"] = 9}
-- where the value associated with each entry is the amount of damage dealt. Groups are permitted. Note that negative damage represents "healing" radiation.
-- attenuated_by has the following similar format:
-- {["group:stone"] = 0.25, ["default:steelblock"] = 0}
-- where the value is a multiplier that is applied to the damage passing through it. Groups are permitted. Note that you can use values greater than one to make a node type magnify damage instead of attenuating it.

-- Commmon function for looking up an emitted_by or attenuated_by value for a node
local get_val = function(node_name, target_names, target_groups)
	if target_names then
		local name_val = target_names[node_name]
		if name_val ~= nil then return name_val end
	end

	if target_groups then
		local node_def = minetest.registered_nodes[node_name]
		local node_groups = node_def.groups
		if node_groups then
			for group, _ in pairs(node_groups) do
				local group_val = target_groups[group]
				if group_val ~= nil then return group_val end -- returns the first group value it finds, if multiple apply it's undefined which will be selected
			end
		end
	end

	return nil
end

local attenuation_check

if Raycast ~= nil then -- version 0.5 of Minetest adds the Raycast class, use that.

-- Gets three raycasts from the faces of the nodes facing the player.
local get_raycasts = function(node_pos, player_pos)
	local results = {}
	if player_pos.x > node_pos.x then
		table.insert(results, Raycast({x=node_pos.x+0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
	else
		table.insert(results, Raycast({x=node_pos.x-0.51, y=node_pos.y, z=node_pos.z}, player_pos, false, true))
	end

	if player_pos.y > node_pos.y then
		table.insert(results, Raycast({y=node_pos.y+0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
	else
		table.insert(results, Raycast({y=node_pos.y-0.51, x=node_pos.x, z=node_pos.z}, player_pos, false, true))
	end

	if player_pos.z > node_pos.z then
		table.insert(results, Raycast({z=node_pos.z+0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
	else
		table.insert(results, Raycast({z=node_pos.z-0.51, x=node_pos.x, y=node_pos.y}, player_pos, false, true))
	end
	return results
end

attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)

	-- First check a simple degenerate case; if there are no special modifier nodes and the default attenuation
	-- is 1 then we don't need to bother with any detailed checking, the damage goes through unmodified.
	if default_attenuation == 1 and attenuation_nodes == nil and attenuation_groups == nil then return 1 end

	local raycasts = get_raycasts(node_pos, player_pos)

	local farthest_from_zero = 0
	for _, raycast in pairs(raycasts) do
		local current_attenuation = 1
		for ray_node in raycast do
			local ray_node_name = minetest.get_node(ray_node.under).name
			local ray_node_val = get_val(ray_node_name, attenuation_nodes, attenuation_groups)
			if ray_node_val == nil then ray_node_val = default_attenuation end
			current_attenuation = current_attenuation * ray_node_val
			if current_attenuation == 0 then break end -- once we hit zero no further checks are needed, it will never change.
		end

		-- By always selecting the farthest value from zero we accomodate both "healing" and "harmful" radiation
		-- and always let the most impactful value of either type through.
		-- If you've got both positive and negative modifiers (for example, if you've got a magical node that turns
		-- harmful radiation into healing radiation when it passes through) this could result in somewhat erratic effects.
		-- But that's part of the fun, eh? Players will just need to design and use their healing ray carefully.
		if math.abs(current_attenuation) > math.abs(farthest_from_zero) then
			farthest_from_zero = current_attenuation
		end
	end
	return farthest_from_zero
end

else

-- Pre-Minetest 0.5 version. Attenuation_nodes and attenuation_groups are ignored
attenuation_check = function(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)

	if default_attenuation == 1 then return 1 end -- if default_attenuation is 1, don't attenuate.

	-- otherwise, it's all-or-nothing:
	if player_pos.y > node_pos.y then
		if minetest.line_of_sight({y=node_pos.y+0.51, x=node_pos.x, z=node_pos.z}, player_pos) then return 1 end
	else
		if minetest.line_of_sight({y=node_pos.y-0.51, x=node_pos.x, z=node_pos.z}, player_pos) then return 1 end
	end

	if player_pos.x > node_pos.x then
		if minetest.line_of_sight({x=node_pos.x+0.51, y=node_pos.y, z=node_pos.z}, player_pos) then return 1 end
	else
		if minetest.line_of_sight({x=node_pos.x-0.51, y=node_pos.y, z=node_pos.z}, player_pos) then return 1 end
	end

	if player_pos.z > node_pos.z then
		if minetest.line_of_sight({z=node_pos.z+0.51, x=node_pos.x, y=node_pos.y}, player_pos) then return 1 end
	else
		if minetest.line_of_sight({z=node_pos.z-0.51, x=node_pos.x, y=node_pos.y}, player_pos) then return 1 end
	end
	return 0
end

end

radiant_damage.registered_damage_types = {}

-- This method will update the registered_damage_types table with new values, keeping all the parameters
-- consistent and in proper relation to each other.
local update_damage_type = function(damage_name, new_def)

	if radiant_damage.registered_damage_types[damage_name] == nil then
		radiant_damage.registered_damage_types[damage_name] = {}
	end
	local damage_def = radiant_damage.registered_damage_types[damage_name]

	-- Interval
	if new_def.interval ~= nil then
		damage_def.interval = new_def.interval
	elseif damage_def.interval == nil then
		damage_def.interval = 1
	end

	-- Inverse square falloff
	if new_def.inverse_square_falloff ~= nil then
		damage_def.inverse_square_falloff = new_def.inverse_square_falloff
	elseif damage_def.inverse_square_falloff == nil then
		damage_def.inverse_square_falloff = true
	end

	-- Default attenuation value
	if new_def.default_attenuation ~= nil then
		damage_def.default_attenuation = new_def.default_attenuation
	elseif damage_def.default_attenuation == nil then
		damage_def.default_attenuation = 0
	end

	-- Above Only
	damage_def.above_only = new_def.above_only -- default to false

	-- on_damage callback
	damage_def.on_damage = new_def.on_damage

	-- it is efficient to split the emission and attenuation data into separate node and group maps.

	-- Emitted by
	damage_def.emission_nodes = damage_def.emission_nodes or {}
	damage_def.emission_groups = damage_def.emission_groups or {}
	for nodename, damage in pairs(new_def.emitted_by) do
		if damage == 0 then damage = nil end -- causes removal of damage-0 node types
		if string.sub(nodename, 1, 6) == "group:" then
			damage_def.emission_groups[string.sub(nodename, 7)] = damage -- omit the "group:" prefix
		else
			damage_def.emission_nodes[nodename] = damage
		end
	end

	damage_def.nodenames = damage_def.nodenames or {} -- for use with minetest.find_nodes_in_area
	for nodename, damage in pairs(new_def.emitted_by) do
		local handled = false
		for i, v in ipairs(damage_def.nodenames) do
			if v == nodename then
				if damage == 0 then
					table.remove(damage_def.nodenames, i)
				end
				handled = true
				break
			end
		end

		if not handled then
			table.insert(damage_def.nodenames, nodename)
		end
	end

	-- These remain nil unless some valid data is provided.
	if new_def.attenuated_by and Raycast then
		for nodename, attenuation in pairs(new_def.attenuated_by) do
			damage_def.attenuation_nodes = damage_def.attenuation_nodes or {}
			damage_def.attenuation_groups = damage_def.attenuation_groups or {}
			if string.sub(nodename, 1, 6) == "group:" then
				damage_def.attenuation_groups[string.sub(nodename, 7)] = attenuation -- omit the "group:" prefix
			else
				damage_def.attenuation_nodes[nodename] = attenuation
			end
		end
	end

	-- remove any attenuation nodes or groups that match the default attenuation, they're pointless.
	if damage_def.attenuation_groups then
		for node, attenuation in pairs(damage_def.attenuation_groups) do
			if attenuation == damage_def.default_attenuation then
				damage_def.attenuation_groups[node] = nil
			end
		end
	end
	if damage_def.attenuation_nodes then
		for node, attenuation in pairs(damage_def.attenuation_nodes) do
			if attenuation == damage_def.default_attenuation then
				damage_def.attenuation_nodes[node] = nil
			end
		end
	end

	-- Range
	if new_def.range ~= nil then
		damage_def.absolute_range = true
		damage_def.range = new_def.range
	elseif damage_def.inverse_square_falloff and not damage_def.absolute_range then
		damage_def.range = 0
		for _, damage in pairs(damage_def.emission_nodes) do
			damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
		end
		for _, damage in pairs(damage_def.emission_groups) do
			damage_def.range = math.max(math.sqrt(math.abs(damage*8)), damage_def.range) -- use the maximum damage-dealer to determine range.
		end
	end

	return damage_def
end

radiant_damage.override_radiant_damage = function(damage_name, damage_def)
	if radiant_damage.registered_damage_types[damage_name] then
		update_damage_type(damage_name, damage_def)
	elseif minetest.settings:get_bool("enable_damage") then
		minetest.log("error", "Attempt was made to override unregistered radiant_damage type " .. damage_name)
	end
end

radiant_damage.register_radiant_damage = function(damage_name, damage_def)
	if not minetest.settings:get_bool("enable_damage") then return end -- don't bother if enable_damage isn't set.

	if radiant_damage.registered_damage_types[damage_name] then
		minetest.log("error", "Attempt was made to register the already-registered radiant_damage type " .. damage_name)
		return
	end

	local damage_def = update_damage_type(damage_name, damage_def)

	local range = damage_def.range
	local above_only = damage_def.above_only
	local nodenames = damage_def.nodenames
	local default_attenuation = damage_def.default_attenuation
	local attenuation_nodes = damage_def.attenuation_nodes
	local attenuation_groups = damage_def.attenuation_groups
	local emission_nodes = damage_def.emission_nodes
	local emission_groups = damage_def.emission_groups
	local inverse_square_falloff = damage_def.inverse_square_falloff
	local on_damage = damage_def.on_damage
	local interval = damage_def.interval

	local timer = 0

	minetest.register_globalstep(function(dtime)
		timer = timer + dtime
		if timer >= interval then
			timer = timer - interval

			for _, player in pairs(minetest.get_connected_players()) do
				local player_pos = player:get_pos() -- node player's feet are in this location. Add 1 to y to get chest height, more intuitive that way
				player_pos.y = player_pos.y + 1

				local rounded_pos = vector.round(player_pos)
				local nearby_nodes
				if above_only then
					nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, {x=0, y= -range, z=0}), rounded_pos, nodenames)
				else
					nearby_nodes = minetest.find_nodes_in_area(vector.add(rounded_pos, -range), vector.add(rounded_pos, range), nodenames)
				end

				local total_damage = 0

				for _, node_pos in ipairs(nearby_nodes) do
					local distance
					if above_only then
						distance = math.max(player_pos.y - node_pos.y, 1)
					else
						distance = math.max(vector.distance(player_pos, node_pos), 1) -- clamp to 1 to avoid inverse falloff causing crazy huge damage when standing inside a node
					end

					if distance <= range then
						local attenuation = attenuation_check(node_pos, player_pos, default_attenuation, attenuation_nodes, attenuation_groups)
						if attenuation ~= 0 then
							local damage = get_val(minetest.get_node(node_pos).name, emission_nodes, emission_groups)
							if inverse_square_falloff then
								total_damage = total_damage + (damage / (distance * distance)) * attenuation
							else
								total_damage = total_damage + damage * attenuation
							end
						end
					end
				end

				if on_damage == nil then
					total_damage = math.floor(total_damage)
					if total_damage ~= 0 then
						minetest.log("action", player:get_player_name() .. " takes " .. tostring(total_damage) .. " damage from " .. damage_name .. " radiant damage at " .. minetest.pos_to_string(rounded_pos))
						player:set_hp(player:get_hp() - total_damage)
					end
				else
					on_damage(player, total_damage, rounded_pos)
				end
			end
		end
	end)
end

if radiant_damage.config.enable_heat_damage then
	local on_fire_damage
	if minetest.get_modpath("3d_armor") and armor ~= nil then
		-- 3d_armor uses a strange fire protection system different from all the rest, wherein
		-- its armor protects wholly against some heat sources and not at all against others
		-- based on how "hot" they are and how strong against fire the armor is.
		-- Level 1 protects against a wall torch, level 3 protects against a basic fire, and level 5 protects against lava.

		-- Converting this into a standard armor type is going to require some arbitrary decisions.
		-- My decision is: level 5 protection should reduce the default damage from lava immersion from 8 to 0.5 hp (0.0625 multiplier).
		-- Level 3 protection reduces the default damage from basic flame from 4 to 0.5 hp (0.125 multiplier)
		-- Torches don't do damage in default so I will ignore that.
		-- Level 0 has a damage multiplier of 1.

		-- That gives us three data points: (0,1), (3,0.125), (5,0.0625). Fitting that to an exponential curve gives us:
		-- y = 0.0481417 + 0.9518583*e^(-0.8388176*x)
		-- Which looks about right on a graph, and "looks about right on a graph" is good enough for me.
		on_fire_damage = function(player, damage, pos)
			local fire_protection = armor.def[player:get_player_name()].fire
			local fire_multiplier = 1
			if fire_protection then
				fire_multiplier = math.min(1, 0.0481417 + 0.9518583 * math.exp(-0.8388176*fire_protection))
			end
			-- If the player also has conventional fire armor, use whatever's better.
			local fire_armor = player:get_armor_groups().fire
			if fire_armor then
				fire_multiplier = math.min(fire_multiplier, fire_armor/100)
			end
			damage = math.floor(damage * fire_multiplier)
			if damage > 0 then
				minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
				player:set_hp(player:get_hp() - damage)
				minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
			end
		end
	else
		on_fire_damage = function(player, damage, pos)
			local fire_armor = player:get_armor_groups().fire
			if fire_armor then
				damage = damage * fire_armor / 100
			end
			damage = math.floor(damage)
			if damage > 0 then
				minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from heat radiant damage at " .. minetest.pos_to_string(pos))
				player:set_hp(player:get_hp() - damage)
				minetest.sound_play("radiant_damage_sizzle", {gain = math.min(1, damage/10), pos=pos})
			end
		end
	end

	radiant_damage.register_radiant_damage("heat", {
		interval = 1,
		emitted_by = {["group:lava"] = radiant_damage.config.lava_damage, ["fire:basic_flame"] = radiant_damage.config.fire_damage, ["fire:permanent_flame"] = radiant_damage.config.fire_damage},
		inverse_square_falloff = true,
		default_attenuation = 0, -- heat is blocked by anything.
		on_damage = on_fire_damage,
	})
end

if radiant_damage.config.enable_mese_damage then

local shields = {"default:steelblock", "default:copperblock", "default:tinblock", "default:bronzeblock", "default:goldblock"}
local amplifiers = {"default:diamondblock", "default:coalblock"}

for _, shielding_node in ipairs(shields) do
	local node_def = minetest.registered_nodes[shielding_node]
	if node_def then
		local new_groups = node_def.groups or {}
		new_groups.mese_radiation_shield = 1
		minetest.override_item(shielding_node, {groups=new_groups})
	end
end
for _, amp_node in ipairs(amplifiers) do
	local node_def = minetest.registered_nodes[amp_node]
	if node_def then
		local new_groups = node_def.groups or {}
		new_groups.mese_radiation_amplifier = 1
		minetest.override_item(amp_node, {groups=new_groups})
	end
end

local on_radiation_damage = function(player, damage, pos)
	local radiation_multiplier = player:get_armor_groups().radiation
	if radiation_multiplier then
		damage = damage * radiation_multiplier / 100
	end
	damage = math.floor(damage)
	if damage > 0 then
		minetest.log("action", player:get_player_name() .. " takes " .. tostring(damage) .. " damage from mese radiation damage at " .. minetest.pos_to_string(pos))
		player:set_hp(player:get_hp() - damage)
		minetest.sound_play({name = "radiant_damage_geiger", gain = math.min(1, damage/10)}, {to_player=player:get_player_name()})
	end
end

radiant_damage.register_radiant_damage("mese", {
	interval = radiant_damage.config.mese_interval,
	inverse_square_falloff = true,
	emitted_by = {["default:stone_with_mese"] = radiant_damage.config.mese_damage, ["default:mese"] = radiant_damage.config.mese_damage * 9},
	attenuated_by = {["group:stone"] = 0.5, ["group:mese_radiation_shield"] = 0.1, ["group:mese_radiation_amplifier"] = 4},
	default_attenuation = 0.9,
	on_damage = on_radiation_damage,
})
end


code:
name = radiant_damage
author = FaceDeer
description = A mod that allows nodes to damage players at a distance
depends =
optional_depends = default, fire, 3d_armor


code:
radiant_damage_enable_heat_damage (Enable radiant heat damage) bool false
radiant_damage_lava_damage (Damage dealt per second when standing directly adjacent to one lava node) int 8
radiant_damage_fire_damage (Damage dealt per second when standing directly adjacent to one fire node) int 2

radiant_damage_enable_mese_damage (Enable mese ore radiation damage) bool false
radiant_damage_mese_interval (Number of seconds between mese radiation damage checks) int 5
radiant_damage_mese_damage (Damage dealt per second when standing directly adjacent to one mese ore node) int 2


code:
Minetest Game mod: sfinv
========================
See license.txt for license information.

Simple Fast Inventory.
A cleaner, simpler, solution to having an advanced inventory in Minetest.
See game_api.txt for this mod's API.
Available for use outside of MTG here:
https://forum.minetest.net/viewtopic.php?t=19765

Authors of source code
----------------------
rubenwardy (MIT)

Authors of media
----------------
paramat (CC BY-SA 3.0):
  sfinv_crafting_arrow.png - derived from a texture by BlockMen (CC BY-SA 3.0)


code:
sfinv = {
	pages = {},
	pages_unordered = {},
	contexts = {},
	enabled = true
}

function sfinv.register_page(name, def)
	assert(name, "Invalid sfinv page. Requires a name")
	assert(def, "Invalid sfinv page. Requires a def[inition] table")
	assert(def.get, "Invalid sfinv page. Def requires a get function.")
	assert(not sfinv.pages[name], "Attempt to register already registered sfinv page " .. dump(name))

	sfinv.pages[name] = def
	def.name = name
	table.insert(sfinv.pages_unordered, def)
end

function sfinv.override_page(name, def)
	assert(name, "Invalid sfinv page override. Requires a name")
	assert(def, "Invalid sfinv page override. Requires a def[inition] table")
	local page = sfinv.pages[name]
	assert(page, "Attempt to override sfinv page " .. dump(name) .. " which does not exist.")
	for key, value in pairs(def) do
		page[key] = value
	end
end

function sfinv.get_nav_fs(player, context, nav, current_idx)
	-- Only show tabs if there is more than one page
	if #nav > 1 then
		return "tabheader[0,0;sfinv_nav_tabs;" .. table.concat(nav, ",") ..
				";" .. current_idx .. ";true;false]"..
				"image[-.5,-.3;11,.1;sfinv_underline.png]"
	else
		return ""
	end
end
local theme_inv = [[
		image[0,5.2;1,1;gui_hb_bg.png]
		image[1,5.2;1,1;gui_hb_bg.png]
		image[2,5.2;1,1;gui_hb_bg.png]
		image[3,5.2;1,1;gui_hb_bg.png]
		image[4,5.2;1,1;gui_hb_bg.png]
		image[5,5.2;1,1;gui_hb_bg.png]
		image[6,5.2;1,1;gui_hb_bg.png]
		image[7,5.2;1,1;gui_hb_bg.png]
		list[current_player;main;0,5.2;8,1;]
		list[current_player;main;0,6.35;8,3;8]
	]]

function sfinv.make_formspec(player, context, content, show_inv, size)
	local tmp = {
		size or "size[8,9.1]",
		"background9[-2,-2;12,13.1;sfinv_bg.png;false;64,96]",
		sfinv.get_nav_fs(player, context, context.nav_titles, context.nav_idx),
		show_inv and theme_inv or "",
		content
	}
	return table.concat(tmp, "")
end

function sfinv.get_homepage_name(player)
	return "sfinv:crafting"
end

function sfinv.get_formspec(player, context)
	-- Generate navigation tabs
	local nav = {}
	local nav_ids = {}
	local current_idx = 1
	for i, pdef in pairs(sfinv.pages_unordered) do
		if not pdef.is_in_nav or pdef:is_in_nav(player, context) then
			nav[#nav + 1] = pdef.title
			nav_ids[#nav_ids + 1] = pdef.name
			if pdef.name == context.page then
				current_idx = #nav_ids
			end
		end
	end
	context.nav = nav_ids
	context.nav_titles = nav
	context.nav_idx = current_idx

	-- Generate formspec
	local page = sfinv.pages[context.page] or sfinv.pages["404"]
	if page then
		return page:get(player, context)
	else
		local old_page = context.page
		local home_page = sfinv.get_homepage_name(player)

		if old_page == home_page then
			minetest.log("error", "[sfinv] Couldn't find " .. dump(old_page) ..
					", which is also the old page")

			return ""
		end

		context.page = home_page
		assert(sfinv.pages[context.page], "[sfinv] Invalid homepage")
		minetest.log("warning", "[sfinv] Couldn't find " .. dump(old_page) ..
				" so switching to homepage")

		return sfinv.get_formspec(player, context)
	end
end

function sfinv.get_or_create_context(player)
	local name = player:get_player_name()
	local context = sfinv.contexts[name]
	if not context then
		context = {
			page = sfinv.get_homepage_name(player)
		}
		sfinv.contexts[name] = context
	end
	return context
end

function sfinv.set_context(player, context)
	sfinv.contexts[player:get_player_name()] = context
end

function sfinv.set_player_inventory_formspec(player, context)
	local fs = sfinv.get_formspec(player,
			context or sfinv.get_or_create_context(player))
	player:set_inventory_formspec(fs)
end

function sfinv.set_page(player, pagename)
	local context = sfinv.get_or_create_context(player)
	local oldpage = sfinv.pages[context.page]
	if oldpage and oldpage.on_leave then
		oldpage:on_leave(player, context)
	end
	context.page = pagename
	local page = sfinv.pages[pagename]
	if page.on_enter then
		page:on_enter(player, context)
	end
	sfinv.set_player_inventory_formspec(player, context)
end

function sfinv.get_page(player)
	local context = sfinv.contexts[player:get_player_name()]
	return context and context.page or sfinv.get_homepage_name(player)
end

minetest.register_on_joinplayer(function(player)
	if sfinv.enabled then
		sfinv.set_player_inventory_formspec(player)
	end
end)

minetest.register_on_leaveplayer(function(player)
	sfinv.contexts[player:get_player_name()] = nil
end)

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "" or not sfinv.enabled then
		return false
	end

	-- Get Context
	local name = player:get_player_name()
	local context = sfinv.contexts[name]
	if not context then
		sfinv.set_player_inventory_formspec(player)
		return false
	end

	-- Was a tab selected?
	if fields.sfinv_nav_tabs and context.nav then
		local tid = tonumber(fields.sfinv_nav_tabs)
		if tid and tid > 0 then
			local id = context.nav[tid]
			local page = sfinv.pages[id]
			if id and page then
				sfinv.set_page(player, id)
			end
		end
	else
		-- Pass event to page
		local page = sfinv.pages[context.page]
		if page and page.on_player_receive_fields then
			return page:on_player_receive_fields(player, context, fields)
		end
	end
end)


code:
-- sfinv/init.lua

dofile(minetest.get_modpath("sfinv") .. "/api.lua")

-- Load support for MT game translation.
local S = minetest.get_translator("sfinv")

sfinv.register_page("sfinv:crafting", {
	title = S("Crafting"),
	icon = "crafting_icon.png",
	get = function(self, player, context)
		return sfinv.make_formspec(player, context, [[
				style_type[list;size=1,spacing=.5]
				list[current_player;craft;1.75,0.5;3,3;]
				list[current_player;craftpreview;5.75,1.5;1,1;]
				image[4.75,1.5;1,1;sfinv_crafting_arrow.png]
				listring[current_player;main]
				listring[current_player;craft]
			]], true)
	end
})


code:
# textdomain:sfinv
Crafting=Fertigung


code:
# textdomain:sfinv
Crafting=Creación


code:
# textdomain:sfinv
Crafting=Artisanat


code:
# textdomain: sfinv
Crafting=Kerajinan


code:
# textdomain:sfinv
Crafting=Pertukangan


code:
# textdomain:sfinv
Crafting=Крафтинг


code:
# textdomain:sfinv
Crafting=合成


code:
# textdomain: sfinv
Crafting=


code:
name = sfinv
description = Minetest Game mod: sfinv


code:
Configuration for Better HUD
----------------------------

The Better HUD mod positions the stat displays according to
a 'hud.conf' file in its mod directory. If you wish to shift
the positions of the stat displays around, you need to create or
edit the 'hud.conf' file.

There should be two files in this mod directory, 'hud.conf.with_hunger'
and 'hud.conf.no_hunger'. These can be used as the basis for your
'hud.conf', depending on whether you are also using the Hunger mod
(which adds a hunger display), or not.

The positions of the displays in those files correspond to the
default settings in Better HUD. In most cases, it's enough to
copy one of these files as 'hud.conf' in the Better HUD mod
directory 'mods/hud'.


code:
Thirsty [thirsty]
=================

A Minetest mod that adds a "thirst" mechanic.

Version: 0.10.2

License:
  Code: LGPL 2.1 (see included LICENSE file)
  Textures: CC-BY-SA (see http://creativecommons.org/licenses/by-sa/4.0/)

Report bugs or request help on the forum topic.

Description
-----------

This is a mod for MineTest. It adds a thirst mechanic to the
game, similar to many hunger mods (but independent of them).
Players will slowly get thirstier over time, and will need to
drink or suffer damage.

The point of this mod is not to make the game more realistic,
or harder. The point is to have another mechanic that rewards
preparation and infrastructure. Players will now have an incentive
to build their base next to water (or add some water to their base),
and/or take some water with them when mining or travelling.

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", meaning a full bar represents full hydration, not full
thirst. Players lose hydration (or "hydro points") over time, and
gain hydration when drinking.

Current behavior
----------------

**Tier 0**: stand in water (running or standing) to slowly drink.
You may not move during drinking (or you could cross an ocean without
getting thirsty).

**Tier 1**: use a container (e.g. from `vessels`) on water to instantly
fill your hydration. Craftable wooden bowl included.

**Tier 2**: craftable canteens: steel canteen holds two full hydration
bars, bronze canteen three bars worth of water.

**Tier 3**: placeable drinking fountain / wash basin node: instantly
fills your hydration when used.

**Tier 4+**: placeable fountain node(s) to fill the hydration of all
players within range. Placing more nodes increases the range.

**Tier 5**: craftable trinkets/gadgets/amulets that constantly keep your
hydration filled when in your inventory, solving your thirst problem
once and for all.

API
---
Some functions of interest:

* thirsty.drink(player, amount, [max]) : instantly drink a bit (up to a max value, default 20)
* thirsty.get_hydro(player) : returns the current hydration of a player
* thirsty.set_thirst_factor(player, factor) : how fast does the given player get thirsty (default is 1.0)
* thirsty.get_thirst_factor(player) : returns the current thirst factor of a player

"player" refers to a player object, i.e. with a get_player_name() method.

Future plans
------------
Better configurability and an API.

Dependencies
------------
* default (optional but needed for included components)
* bucket (optional but needed for some included components)
* hud (optional): https://forum.minetest.net/viewtopic.php?f=11&t=6342 (see HUD.txt for configuration)
* hudbars (optional): https://forum.minetest.net/viewtopic.php?f=11&t=11153
* vessels (optional): https://forum.minetest.net/viewtopic.php?id=2574

Installation
------------

Unzip the archive, rename the folder to to `thirsty` and
place it in minetest/mods/

(  Linux: If you have a linux system-wide installation place
    it in ~/.minetest/mods/.  )

(  If you only want this to be used in a single world, place
    the folder in worldmods/ in your worlddirectory.  )

For further information or help see:
http://wiki.minetest.com/wiki/Installing_Mods


code:
--[[

Default components for Thirsty.

These are nodes and items that "implement" the functionality
from functions.lua

See init.lua for license.

]]

--[[

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.

]]

if minetest.get_modpath("vessels") and thirsty.config.register_vessels then
    -- add "drinking" to vessels
    thirsty.augment_item_for_drinking('vessels:drinking_glass', 22)
    thirsty.augment_item_for_drinking('vessels:glass_bottle', 24)
    thirsty.augment_item_for_drinking('vessels:steel_bottle', 26)
end

if minetest.get_modpath("default") and thirsty.config.register_bowl then
    -- 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),
    })

    minetest.register_craft({
        output = "thirsty:wooden_bowl",
        recipe = {
            {"group:wood", "", "group:wood"},
            {"default:stick", "group:wood", "default:stick"}
        }
    })
end

--[[

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

]]

if minetest.get_modpath("default") and thirsty.config.register_canteens then

    minetest.register_tool('thirsty:wood_canteen', {
        description = 'Wooden Canteen',
        inventory_image = "wood_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })
    minetest.register_tool('thirsty:bronze_canteen', {
        description = 'Bronze Canteen',
        inventory_image = "bronze_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })

    minetest.register_tool('thirsty:steel_canteen', {
        description = 'Steel Canteen',
        inventory_image = "steel_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })

    minetest.register_tool('thirsty:silver_canteen', {
        description = 'Silver Canteen',
        inventory_image = "silver_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })
    minetest.register_tool('thirsty:gold_canteen', {
        description = 'Gold Canteen',
        inventory_image = "gold_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })
    minetest.register_tool('thirsty:diamond_canteen', {
        description = 'Diamond Canteen',
        inventory_image = "diamond_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })
    minetest.register_tool('thirsty:mithril_canteen', {
        description = 'Mithril Canteen',
        inventory_image = "mithril_canteen.png",
        liquids_pointable = true,
        stack_max = 1,
        on_use = thirsty.on_use(nil),
    })
    minetest.register_craft({
        output = "thirsty:wood_canteen",
        recipe = {
            { "group:wood", ""},
            { "group:wood", "group:wood"},
            { "group:wood", "group:wood"}
        }
    })
    minetest.register_craft({
        output = "thirsty:bronze_canteen",
        recipe = {
            { "group:wood", ""},
            { "default:bronze_ingot", "default:bronze_ingot"},
            { "default:bronze_ingot", "default:bronze_ingot"}
        }
    })
    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:silver_canteen",
        recipe = {
            { "group:wood", ""},
            { "moreores:silver_ingot", "moreores:silver_ingot"},
            { "moreores:silver_ingot", "moreores:silver_ingot"}
        }
    })
    minetest.register_craft({
        output = "thirsty:gold_canteen",
        recipe = {
            { "group:wood", ""},
            { "default:gold_ingot", "default:gold_ingot"},
            { "default:gold_ingot", "default:gold_ingot"}
        }
    })
    minetest.register_craft({
        output = "thirsty:diamond_canteen",
        recipe = {
            { "group:wood", ""},
            { "default:diamond", "default:diamond"},
            { "default:diamond", "default:diamond"}
        }
    })
    minetest.register_craft({
        output = "thirsty:mithril_canteen",
        recipe = {
            { "group:wood", ""},
            { "moreores:mithril_ingot", "moreores:mithril_ingot"},
            { "moreores:mithril_ingot", "moreores:mithril_ingot"}
        }
    })

end

--[[

Tier 3

]]

if minetest.get_modpath("default") and minetest.get_modpath("mesecraft_bucket") and thirsty.config.register_drinking_fountain then

    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", "mesecraft_bucket:bucket_water", "default:stone"},
            { "", "default:stone", ""},
            { "", "default:stone", ""}
        }
    })

end

--[[

Tier 4+: the water fountains, plus extenders

]]

if minetest.get_modpath("default") and minetest.get_modpath("mesecraft_bucket") and thirsty.config.register_fountains then

    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 },
    })

    minetest.register_craft({
        output = "thirsty:water_fountain",
        recipe = {
            { "default:copper_ingot", "mesecraft_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 = {
            { "", "mesecraft_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 = thirsty.fountain_abm,
    })

end

--[[

Tier 5

These amulets don't do much; the actual code is above, where
they are searched for in player's inventories

]]

if minetest.get_modpath("default") and minetest.get_modpath("mesecraft_bucket") and thirsty.config.register_amulets then

    minetest.register_craftitem('thirsty:injector', {
        description = 'Water injector',
        inventory_image = 'thirsty_injector.png',
    })
    minetest.register_craft({
        output = "thirsty:injector",
        recipe = {
            { "default:diamond", "default:mese_crystal", "default:diamond"},
            { "default:mese_crystal", "mesecraft_bucket:bucket_water", "default:mese_crystal"},
            { "default:diamond", "default:mese_crystal", "default:diamond"}
        }
    })

    minetest.register_craftitem('thirsty:extractor', {
        description = 'Water extractor',
        inventory_image = 'thirsty_extractor.png',
    })
    minetest.register_craft({
        output = "thirsty:extractor",
        recipe = {
            { "default:mese_crystal", "default:diamond", "default:mese_crystal"},
            { "default:diamond", "mesecraft_bucket:bucket_water", "default:diamond"},
            { "default:mese_crystal", "default:diamond", "default:mese_crystal"}
        }
    })

end


code:
--[[

Configuration from default, moddir and worlddir, in that order.

See init.lua for license.

]]

-- change these for other mods
local M = thirsty
local modname = 'thirsty'
local fileroot = modname

-- make sure config exists; keep constant reference to it
local C = M.config or {}
M.config = C

local function try_config_file(filename)
    --print("Config from "..filename)
    local file, err = io.open(filename, 'r')
    if file then
        file:close() -- was just for checking existance
        local confcode, err = loadfile(filename)
        if confcode then
            confcode()
            if C ~= M.config then
                -- M.config was overriden, merge
                for key, value in pairs(M.config) do
                    if type(value) == 'table' and type(C[key]) == 'table' and not value.CLEAR then
                        for k, v in pairs(value) do
                            C[key][k] = value[k]
                        end
                    else
                        -- copy (not a table, or asked to clear)
                        C[key] = value
                    end
                end
            else
                -- no override? Empty, or file knows what it is doing.
            end
        else
            minetest.log("error", "Could not load " .. filename .. ": " .. err)
        end
    end
end

-- read starting configuration from <modname>.default.conf
try_config_file(minetest.get_modpath(modname) .. "/" .. fileroot .. ".default.conf")

-- next, install-specific copy in modpath
try_config_file(minetest.get_modpath(modname) .. "/" .. fileroot .. ".conf")

-- last, world-specific copy in worldpath
try_config_file(minetest.get_worldpath() .. "/" .. fileroot .. ".conf")

-- remove any special keys from tables
for key, value in pairs(C) do
    if type(value) == 'table' then
        value.CLEAR = nil
    end
end

-- write back
M.config = C


code:
default?
bucket?
hud?
hudbars?
vessels?


code: A mod that adds a "thirst" mechanic, similar to hunger.


code:
--[[

Core functions for Thirsty.

See init.lua for license.

]]

local PPA = thirsty.persistent_player_attributes
local damage_enabled = minetest.settings:get_bool("enable_damage")

PPA.register({
    name = 'thirsty_hydro',
    min = 0,
    max = 50,
    default = 20,
})

function thirsty.on_joinplayer(player)
    local name = player:get_player_name()
    -- default entry for new players
    if not thirsty.players[name] then
        local pos = player:get_pos()
        thirsty.players[name] = {
            last_pos = math.floor(pos.x) .. ':' .. math.floor(pos.z),
            time_in_pos = 0.0,
            pending_dmg = 0.0,
            thirst_factor = 0.25,
        }
    end
    thirsty.hud_init(player)
end

function thirsty.on_dieplayer(player)
    local name = player:get_player_name()
    local pl   = thirsty.players[name]
    -- reset after death
    PPA.set_value(player, 'thirsty_hydro', 20)
    pl.pending_dmg = 0.0
    pl.thirst_factor = 0.25
end

--[[

Getters, setters and such

]]

function thirsty.drink(player, value, max)
    -- if max is not specified, assume 20
    if not max then
        max = 20
    end
    local hydro = PPA.get_value(player, 'thirsty_hydro')
    -- test whether we're not *above* max;
    -- this function should not remove any overhydration
    if hydro < max then
        hydro = math.min(hydro + value, max)
        --print("Drinking by "..value.." to "..hydro)
        PPA.set_value(player, 'thirsty_hydro', hydro)
        return true
    end
    return false
end

function thirsty.get_hydro(player)
    return PPA.get_value(player, 'thirsty_hydro')
end

function thirsty.set_thirst_factor(player, factor)
    local name = player:get_player_name()
    local pl = thirsty.players[name]
    pl.thirst_factor = factor
end

function thirsty.get_thirst_factor(player)
    local name = player:get_player_name()
    local pl = thirsty.players[name]
    return pl.thirst_factor
end

--[[

Main Loop (Tier 0)

]]

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

            if player:get_hp() <= 0 then
                -- dead players don't get thirsty, or full for that matter :-P
                break
            end

            local name = player:get_player_name()
            local pos  = player:get_pos()
            local pl = thirsty.players[name]
            local hydro = PPA.get_value(player, 'thirsty_hydro')

            -- 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 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()
                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
            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 = PPA.get_value(player, '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, 20)
                --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
                    PPA.set_value(player, 'thirsty_hydro', hydro - amount)
                    hydro = PPA.get_value(player, 'thirsty_hydro')
                    --print("Lowering hydration by "..amount.." to "..hydro)
                end
            end

            -- should we only update the hud on an actual change?
            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
        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

--[[

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 hydro = PPA.get_value(player, 'thirsty_hydro')
    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"
	   -- Play a sound effect at the players position when filling container with water.
           minetest.sound_play({name = "thirsty_fill_container", gain = 1.5}, {pos=player:getpos(), max_hear_distance = 8})
        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))
		-- play a drinking sound effect at the players position when they drink.
	          minetest.sound_play({name = "thirsty_drink", gain = 1.5}, {pos=player:getpos(), max_hear_distance = 4})
                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?
                    thirsty.drink(player, wear_missing * capacity / 65535.0, 20)
                    hydro = PPA.get_value(player, 'thirsty_hydro')
                end
            end
        end
    end

    -- update HUD if value changed
    if hydro ~= old_hydro then
        thirsty.hud_update(player, 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

--[[

Adapter to add "drink_handler" to any item (node, tool, craftitem).

]]

function thirsty.augment_item_for_drinking( itemname, level )
    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(
        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

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


code:
--[[

Better HUD config file, without Hunger mod.

This file mirrors the default settings of the Better HUD mod,
except that it moves the "breath" bar upwards to make room
for the "thirst" bar.

Use this config file as a starting point if you *don't* have the
Hunger mod enabled.

For more information, see hud.conf.example in the mods/hud
directory.

]]

HUD_SB_SIZE = { x = 24, y = 24 }

--[[ Layout:

    ARMOR | (AIR)
   HEALTH | THIRST

]]

HUD_HEALTH_POS    = { x = 0.5, y = 1 }
HUD_HEALTH_OFFSET = { x = -262, y = -87 }

-- not used
HUD_HUNGER_POS    = { x = 0.5, y = 1 }
HUD_HUNGER_OFFSET = { x = 15, y = -133 }

HUD_AIR_POS       = { x = 0.5, y = 1 }
HUD_AIR_OFFSET    = { x = 15, y = -110 }

HUD_ARMOR_POS     = { x = 0.5, y = 1 }
HUD_ARMOR_OFFSET  = { x = -262, y = -110 }

HUD_THIRST_POS    = { x = 0.5, y = 1 }
HUD_THIRST_OFFSET = { x = 15, y = -87 }


code:
--[[

Better HUD config file, with Hunger mod.

This file mirrors the default settings of the Better HUD mod,
except that it moves the "breath" bar upwards to make room
for the "thirst" bar.

Use this config file as a starting point if you *do* have the
Hunger mod enabled.

For more information, see hud.conf.example in the mods/hud
directory.

]]

HUD_SB_SIZE = { x = 24, y = 24 }

--[[ Layout:

            (AIR)
    ARMOR | THIRST
   HEALTH | HUNGER

]]

HUD_HEALTH_POS    = { x = 0.5, y = 1 }
HUD_HEALTH_OFFSET = { x = -262, y = -87 }

-- At the time of writing, the Hunger mod contains code to swap
-- the positions of "hunger" and "air", so these positions are
-- "un-swapped"...
HUD_HUNGER_POS    = { x = 0.5, y = 1 }
HUD_HUNGER_OFFSET = { x = 15, y = -133 }

HUD_AIR_POS       = { x = 0.5, y = 1 }
HUD_AIR_OFFSET    = { x = 15, y = -87 }

HUD_ARMOR_POS     = { x = 0.5, y = 1 }
HUD_ARMOR_OFFSET  = { x = -262, y = -110 }

HUD_THIRST_POS    = { x = 0.5, y = 1 }
HUD_THIRST_OFFSET = { x = 15, y = -110 }


code:
--[[

HUD definitions for Thirsty

Optionally from one of the supported mods

Any hud needs to define the following functions:

- thirsty.hud_init(player)
  Initialize the HUD for a new player.

- thirsty.hud_update(player, value)
  Display the new value "value" for the given player. "value" is
  a floating point number, not necessarily bounded. You can use the
  "thirsty.hud_clamp(value)" function to get an integer between 0
  and 20.

]]

local PPA = thirsty.persistent_player_attributes

function thirsty.hud_clamp(value)
    if value < 0 then
        return 0
    elseif value > 20 then
        return 20
    else
        return math.ceil(value)
    end
end

if minetest.get_modpath("hudbars") then
    hb.register_hudbar('thirst', 0xffffff, "Hydration", {
        bar = 'thirsty_hudbars_bar.png',
        icon = 'thirsty_cup_100_16.png',
	bgicon = 'thirsty_cup_bgicon_16.png'

    }, 20, 20, false)
    function thirsty.hud_init(player)
        hb.init_hudbar(player, 'thirst',
            thirsty.hud_clamp(PPA.get_value(player, 'thirsty_hydro')),
        20, false)
    end
    function thirsty.hud_update(player, value)
        hb.change_hudbar(player, 'thirst', thirsty.hud_clamp(value), 20)
    end
elseif minetest.get_modpath("hud") then
    -- default positions follow [hud] defaults
    local position = HUD_THIRST_POS or { x=0.5, y=1 }
    local offset   = HUD_THIRST_OFFSET or { x=15, y=-133} -- above AIR
    hud.register('thirst', {
        hud_elem_type = "statbar",
        position = position,
        text = "thirsty_cup_100_24.png",
        background = "thirsty_cup_0_24.png",
        number = 20,
        max = 20,
        size = HUD_SB_SIZE, -- by default { x=24, y=24 },
        offset = offset,
    })
    function thirsty.hud_init(player)
        -- automatic by [hud]
    end
    function thirsty.hud_update(player, value)
        hud.change_item(player, 'thirst', {
            number = thirsty.hud_clamp(value)
        })
    end
else
    -- 'builtin' hud
    function thirsty.hud_init(player)
        -- above breath bar, for now
        local name = player:get_player_name()
        thirsty.players[name].hud_id = player:hud_add({
            hud_elem_type = "statbar",
            position = { x=0.5, y=1 },
            text = "thirsty_cup_100_24.png",
            number = thirsty.hud_clamp(PPA.get_value(player, 'thirsty_hydro')),
            direction = 0,
            size = { x=24, y=24 },
            offset = { x=25, y=-(48+24+16+32)},
        })
    end
    function thirsty.hud_update(player, value)
        local name = player:get_player_name()
        local hud_id = thirsty.players[name].hud_id
        player:hud_change(hud_id, 'number', thirsty.hud_clamp(value))
    end
end


code:
--[[

Thirsty mod [thirsty]
==========================

A mod that adds a "thirst" mechanic, similar to hunger.

Copyright (C) 2015 Ben Deutsch <ben@bendeutsch.de>

License
-------

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".

]]

-- the main module variable
thirsty = {

    -- Configuration variables
    config = {
        -- configuration in thirsty.default.conf
    },

    -- the players' values
    players = {
        --[[
        name = {
            last_pos = '-10:3',
            time_in_pos = 0.0,
            pending_dmg = 0.0,
            thirst_factor = 1.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,
}
local M = thirsty

dofile(minetest.get_modpath('thirsty')..'/configuration.lua')
local C = M.config

dofile(minetest.get_modpath('thirsty')..'/persistent_player_attributes.lua')
local PPA = M.persistent_player_attributes

thirsty.time_next_tick = thirsty.config.tick_time

dofile(minetest.get_modpath('thirsty')..'/hud.lua')
dofile(minetest.get_modpath('thirsty')..'/functions.lua')

minetest.register_on_joinplayer(thirsty.on_joinplayer)
minetest.register_on_dieplayer(thirsty.on_dieplayer)
minetest.register_globalstep(thirsty.main_loop)

dofile(minetest.get_modpath('thirsty')..'/components.lua')


code:
name = thirsty
optional_depends = default, bucket, hud, hudbars, vessels
description = A mod that adds a "thirst" mechanic, similar to hunger.


code:
--[[

Persistent player attributes

]]

-- change this to inject into other module
local M = thirsty

M.persistent_player_attributes = {}
local PPA = M.persistent_player_attributes

--[[
Helper functions that take care of the conversions *and* the
clamping for us
]]

local function _count_for_val(value, def)
    local count = math.floor((value - def.min) / (def.max - def.min) * 65535)
    if count < 0 then count = 0 end
    if count > 65535 then count = 65535 end
    return count
end
local function _val_for_count(count, def)
    local value = count / 65535 * (def.max - def.min) + def.min
    if value < def.min then value = def.min end
    if value > def.max then value = def.max end
    return value
end
-- end helper functions

-- the stash of registered attributes

PPA.defs = {--[[
    name = {
        name = "mymod_attr1",
        min = 0,
        max = 10,
        default = 5,
    },
]]}

PPA.read_cache = {--[[
    player_name = {
        attr1 = value1,
        attr2 = value2,
    },
]]}

--[[
How to register a new attribute, with named parameters:
    PPA.register({ name = "mymod_attr1", min = 0, ... })
]]

PPA.register = function(def)
    PPA.defs[def.name] = {
        name = def.name,
        min = def.min or 0.0,
        max = def.max or 1.0,
        default = def.default or def.min or 0.0,
    }
end

-- The on_joinplayer handler

PPA.on_joinplayer = function(player)
    local inv = player:get_inventory()
    local player_name = player:get_player_name()
    PPA.read_cache[player_name] = {}
    for name, def in pairs(PPA.defs) do
        inv:set_size(name, 1)
        if inv:is_empty(name) then
            -- set default value
            inv:set_stack(name, 1, ItemStack({ name = ":", count = _count_for_val(def.default, def) }))
            -- cache default value
            PPA.read_cache[player_name][name] = def.default
        end
    end
end

minetest.register_on_joinplayer(PPA.on_joinplayer)

--[[ get an attribute, procedural style:
    local attr1 = PPA.get_value(player, "mymod_attr1")
]]

PPA.get_value = function(player, name)
    local player_name = player:get_player_name()
    if PPA.read_cache[player_name][name] == nil then
        local def = PPA.defs[name]
        local inv = player:get_inventory()
        local count = inv:get_stack(name, 1):get_count()
        PPA.read_cache[player_name][name] = _val_for_count(count, def)
    end
    return PPA.read_cache[player_name][name]
end

--[[ set an attribute, procedural style:
    PPA.set_value(player, "mymod_attr1", attr1)
]]

PPA.set_value = function(player, name, value)
    local def = PPA.defs[name]
    local inv = player:get_inventory()
    local player_name = player:get_player_name()
    if value > def.max then value = def.max end
    if value < def.min then value = def.min end
    PPA.read_cache[player_name][name] = value
    inv:set_stack(name, 1, ItemStack({ name = ":", count = _count_for_val(value, def) }))
end


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="16"
   height="16"
   id="svg2"
   version="1.1"
   inkscape:version="0.48.4 r9939"
   sodipodi:docname="bowl.svg"
   inkscape:export-filename="/home/ben/progs/minetest/minetest-thirsty/textures/thirsty_bowl_32.png"
   inkscape:export-xdpi="180"
   inkscape:export-ydpi="180">
  <defs
     id="defs4">
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3824"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3820"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761-8"
       is_visible="true" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#838181"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="31.678384"
     inkscape:cx="6.537478"
     inkscape:cy="7.4852095"
     inkscape:document-units="px"
     inkscape:current-layer="layer1"
     showgrid="true"
     inkscape:window-width="1366"
     inkscape:window-height="744"
     inkscape:window-x="0"
     inkscape:window-y="24"
     inkscape:window-maximized="1"
     inkscape:snap-global="false">
    <inkscape:grid
       type="xygrid"
       id="grid3755"
       empspacing="5"
       visible="true"
       enabled="true"
       snapvisiblegridlinesonly="true" />
  </sodipodi:namedview>
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-1036.3622)">
    <path
       sodipodi:type="arc"
       style="fill:#8e5700;fill-opacity:1;stroke:none"
       id="path3757-8"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,0 0,0 0,0"
       transform="matrix(0.0331251,0,0,0.09946417,-3.2760155,1018.2645)"
       sodipodi:start="0"
       sodipodi:end="3.1415927"
       sodipodi:open="true" />
    <path
       sodipodi:type="arc"
       style="fill:#b36e00;fill-opacity:1;stroke:none"
       id="path3757-8-9"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 c 0,33.02374 -80.05876,59.83946 -179.1288,59.9993 L 340,252.36218 z"
       transform="matrix(0.01804296,0,0,0.09946417,1.8203443,1018.2479)"
       sodipodi:start="0"
       sodipodi:end="1.5659563" />
    <path
       sodipodi:type="arc"
       style="fill:#b36e00;fill-opacity:1;stroke:none"
       id="path3757-8-7"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 340.89962,312.36143 c -99.41001,0.16562 -180.40052,-26.5627 -180.89737,-59.69937 -7.6e-4,-0.0508 -0.001,-0.1017 -0.002,-0.15255 L 340,252.36218 z"
       transform="matrix(0.0331251,0,0,0.09946417,-3.3075828,1018.2795)"
       sodipodi:start="1.5657984"
       sodipodi:end="3.1391372" />
    <path
       sodipodi:type="arc"
       style="fill:#563500;fill-opacity:1;stroke:none"
       id="path3757"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,-33.13708 80.58875,-60 180,-60 99.41125,0 180,26.86292 180,60 z"
       transform="matrix(0.0331251,0,0,0.0326468,-3.2695864,1035.128)" />
  </g>
</svg>


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="16"
   height="16"
   id="svg2"
   version="1.1"
   inkscape:version="0.48.4 r9939"
   sodipodi:docname="canteen.svg"
   inkscape:export-filename="/home/ben/progs/minetest/minetest-thirsty/textures/thirsty_bowl_16.png"
   inkscape:export-xdpi="90"
   inkscape:export-ydpi="90">
  <defs
     id="defs4">
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3824"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3820"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761-8"
       is_visible="true" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#838181"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="31.678384"
     inkscape:cx="7.6921211"
     inkscape:cy="7.4852095"
     inkscape:document-units="px"
     inkscape:current-layer="layer1"
     showgrid="true"
     inkscape:window-width="1366"
     inkscape:window-height="744"
     inkscape:window-x="0"
     inkscape:window-y="24"
     inkscape:window-maximized="1"
     inkscape:snap-global="false">
    <inkscape:grid
       type="xygrid"
       id="grid3755"
       empspacing="5"
       visible="true"
       enabled="true"
       snapvisiblegridlinesonly="true" />
  </sodipodi:namedview>
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-1036.3622)">
    <g
       id="g3784"
       style="fill:#e28b1a;fill-opacity:1">
      <path
         transform="translate(0,1037.3408)"
         d="m 13.005714,8.0134811 a 5.0034118,4.987628 0 1 1 -10.0068236,0 5.0034118,4.987628 0 1 1 10.0068236,0 z"
         sodipodi:ry="4.987628"
         sodipodi:rx="5.0034118"
         sodipodi:cy="8.0134811"
         sodipodi:cx="8.0023022"
         id="path3010"
         style="fill:#e28b1a;fill-opacity:1;stroke:none"
         sodipodi:type="arc" />
      <rect
         transform="translate(0,1036.3622)"
         y="2.0157008"
         x="5.9977808"
         height="1.0101526"
         width="4.0090427"
         id="rect3780"
         style="fill:#e28b1a;fill-opacity:1;stroke:none" />
      <rect
         transform="translate(0,1036.3622)"
         y="3.0258534"
         x="6.976366"
         height="1.9887378"
         width="2.0518723"
         id="rect3782"
         style="fill:#e28b1a;fill-opacity:1;stroke:none" />
    </g>
    <rect
       style="fill:#000000;fill-opacity:1;stroke:none"
       id="rect3789"
       width="2.0203052"
       height="0.97858524"
       x="7.0079331"
       y="1.0371156"
       transform="translate(0,1036.3622)" />
    <rect
       style="fill:#562f00;fill-opacity:1;stroke:none"
       id="rect3791"
       width="11.016976"
       height="2.0203052"
       x="562.01971"
       y="876.90887"
       transform="matrix(0.84395175,0.53641909,-0.53641909,0.84395175,0,0)" />
    <rect
       style="fill:#562f00;fill-opacity:1;stroke:none"
       id="rect3791-4"
       width="11.016976"
       height="2.0203052"
       x="548.4823"
       y="885.47589"
       transform="matrix(-0.84395175,0.53641909,0.53641909,0.84395175,0,0)" />
  </g>
</svg>


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="16"
   height="16"
   id="svg2"
   version="1.1"
   inkscape:version="0.48.4 r9939"
   sodipodi:docname="cup_0.svg"
   inkscape:export-filename="/home/ben/progs/minetest/minetest-thirsty/textures/thirsty_cup_50_32.png"
   inkscape:export-xdpi="180"
   inkscape:export-ydpi="180">
  <defs
     id="defs4">
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3824"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3820"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761-8"
       is_visible="true" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#838181"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="31.678384"
     inkscape:cx="5.2747873"
     inkscape:cy="10.523682"
     inkscape:document-units="px"
     inkscape:current-layer="layer1"
     showgrid="true"
     inkscape:window-width="1366"
     inkscape:window-height="744"
     inkscape:window-x="0"
     inkscape:window-y="24"
     inkscape:window-maximized="1"
     inkscape:snap-global="false">
    <inkscape:grid
       type="xygrid"
       id="grid3755"
       empspacing="5"
       visible="true"
       enabled="true"
       snapvisiblegridlinesonly="true" />
  </sodipodi:namedview>
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-1036.3622)">
    <g
       id="g3901"
       transform="translate(-0.06313453,-0.2841054)">
      <path
         transform="matrix(0.02586702,0,0,0.02586702,-0.759071,1034.7695)"
         d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,-33.13708 80.58875,-60 180,-60 99.41125,0 180,26.86292 180,60 z"
         sodipodi:ry="60"
         sodipodi:rx="180"
         sodipodi:cy="252.36218"
         sodipodi:cx="340"
         id="path3757-7"
         style="fill:none;stroke:#000000;stroke-width:38.65926743;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         sodipodi:type="arc" />
      <path
         sodipodi:nodetypes="cccc"
         inkscape:connector-curvature="0"
         inkscape:original-d="m 3.379651,1041.2974 2.0693612,8.7948 m 5.1734018,0 2.069363,-8.7948"
         inkscape:path-effect="#path-effect3761-8"
         id="path3759-1"
         d="m 3.379651,1041.2974 2.0693612,8.7948 m 5.1734018,0 2.069363,-8.7948"
         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
      <path
         sodipodi:end="12.190499"
         sodipodi:start="6.1411257"
         d="m 518.18677,243.86725 c 14.07487,32.80327 -54.29213,63.1989 -152.70196,67.89052 -98.40984,4.69163 -189.59671,-18.09737 -203.67158,-50.90065 -14.07487,-32.80328 54.29213,-63.1989 152.70196,-67.89053 82.50666,-3.93345 162.32363,11.52802 192.91865,37.37059"
         sodipodi:ry="60"
         sodipodi:rx="180"
         sodipodi:cy="252.36218"
         sodipodi:cx="340"
         id="path3757-2-9"
         style="fill:none;stroke:#000000;stroke-width:69.22486115;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         sodipodi:type="arc"
         transform="matrix(0.0143204,0,0,0.01457205,3.175878,1046.32)"
         sodipodi:open="true" />
    </g>
    <path
       sodipodi:type="arc"
       style="fill:none;stroke:#ffffff;stroke-width:38.65926743;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       id="path3757"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,-33.13708 80.58875,-60 180,-60 99.41125,0 180,26.86292 180,60 z"
       transform="matrix(0.02586702,0,0,0.02586702,-1.6068063,1033.8716)" />
    <path
       style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       d="m 2.5319162,1040.3995 2.0693612,8.7948 m 5.1734015,0 2.0693631,-8.7948"
       id="path3759"
       inkscape:path-effect="#path-effect3761"
       inkscape:original-d="m 2.5319162,1040.3995 2.0693612,8.7948 m 5.1734015,0 2.0693631,-8.7948"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="cccc" />
    <path
       transform="matrix(0.0143204,0,0,0.01457205,2.3281432,1045.4221)"
       sodipodi:type="arc"
       style="fill:none;stroke:#ffffff;stroke-width:69.22486115;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       id="path3757-2"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 519.45086,257.04536 c -7.75934,33.03599 -94.39241,57.72022 -193.50038,55.13378 -99.10797,-2.58645 -173.16068,-31.46414 -165.40134,-64.50013 7.75934,-33.03599 94.39241,-57.72023 193.50038,-55.13378 91.69188,2.39291 163.2237,27.44217 165.87564,58.08691"
       sodipodi:start="0.07813237"
       sodipodi:end="6.2543473"
       sodipodi:open="true" />
  </g>
</svg>


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="16"
   height="16"
   id="svg2"
   version="1.1"
   inkscape:version="0.48.4 r9939"
   sodipodi:docname="cup.svg"
   inkscape:export-filename="/home/ben/progs/minetest/minetest-thirsty/textures/thirsty_cup_100_16.png"
   inkscape:export-xdpi="90"
   inkscape:export-ydpi="90">
  <defs
     id="defs4">
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3824"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3820"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761"
       is_visible="true" />
    <inkscape:path-effect
       effect="spiro"
       id="path-effect3761-8"
       is_visible="true" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#838181"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="31.678384"
     inkscape:cx="5.2747873"
     inkscape:cy="10.523682"
     inkscape:document-units="px"
     inkscape:current-layer="layer1"
     showgrid="true"
     inkscape:window-width="1366"
     inkscape:window-height="744"
     inkscape:window-x="0"
     inkscape:window-y="24"
     inkscape:window-maximized="1"
     inkscape:snap-global="false">
    <inkscape:grid
       type="xygrid"
       id="grid3755"
       empspacing="5"
       visible="true"
       enabled="true"
       snapvisiblegridlinesonly="true" />
  </sodipodi:namedview>
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-1036.3622)">
    <g
       id="g3901"
       transform="translate(-0.06313453,-0.2841054)">
      <path
         transform="matrix(0.02586702,0,0,0.02586702,-0.759071,1034.7695)"
         d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,-33.13708 80.58875,-60 180,-60 99.41125,0 180,26.86292 180,60 z"
         sodipodi:ry="60"
         sodipodi:rx="180"
         sodipodi:cy="252.36218"
         sodipodi:cx="340"
         id="path3757-7"
         style="fill:none;stroke:#000000;stroke-width:38.65926743;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         sodipodi:type="arc" />
      <path
         sodipodi:nodetypes="cccc"
         inkscape:connector-curvature="0"
         inkscape:original-d="m 3.379651,1041.2974 2.0693612,8.7948 m 5.1734018,0 2.069363,-8.7948"
         inkscape:path-effect="#path-effect3761-8"
         id="path3759-1"
         d="m 3.379651,1041.2974 2.0693612,8.7948 m 5.1734018,0 2.069363,-8.7948"
         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
      <path
         sodipodi:open="true"
         sodipodi:end="3.1415927"
         sodipodi:start="0"
         d="m 520,252.36218 c 0,33.13709 -80.58875,60 -180,60 -99.41125,0 -180,-26.86291 -180,-60 0,0 0,0 0,0"
         sodipodi:ry="60"
         sodipodi:rx="180"
         sodipodi:cy="252.36218"
         sodipodi:cx="340"
         id="path3757-2-9"
         style="fill:none;stroke:#000000;stroke-width:69.22486115;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         sodipodi:type="arc"
         transform="matrix(0.0143204,0,0,0.01457205,3.175878,1046.32)" />
    </g>
    <path
       style="fill:#0000ff;fill-opacity:1;stroke:none"
       d="m 3.1527235,1042.7275 c 0,0 1.0346817,4.1388 1.5520204,6.4668 1.3456112,1.113 3.7701452,1.0024 5.173404,0 0.5173421,-2.328 1.5520201,-6.4668 1.5520201,-6.4668"
       id="path3822"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="cccc" />
    <path
       transform="matrix(0.02383471,0,0,0.02383471,-0.81511342,1036.4749)"
       sodipodi:type="arc"
       style="fill:#0080ff;fill-opacity:1;stroke:none"
       id="path3757-25"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 a 180,60 0 1 1 -360,0 180,60 0 1 1 360,0 z" />
    <path
       sodipodi:type="arc"
       style="fill:none;stroke:#ffffff;stroke-width:38.65926743;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       id="path3757"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 a 180,60 0 1 1 -360,0 180,60 0 1 1 360,0 z"
       transform="matrix(0.02586702,0,0,0.02586702,-1.6068063,1033.8716)" />
    <path
       style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       d="m 2.5319162,1040.3995 2.0693612,8.7948 m 5.1734015,0 2.0693631,-8.7948"
       id="path3759"
       inkscape:path-effect="#path-effect3761"
       inkscape:original-d="m 2.5319162,1040.3995 2.0693612,8.7948 m 5.1734015,0 2.0693631,-8.7948"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="cccc" />
    <path
       transform="matrix(0.0143204,0,0,0.01457205,2.3281432,1045.4221)"
       sodipodi:type="arc"
       style="fill:none;stroke:#ffffff;stroke-width:69.22486115;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       id="path3757-2"
       sodipodi:cx="340"
       sodipodi:cy="252.36218"
       sodipodi:rx="180"
       sodipodi:ry="60"
       d="m 520,252.36218 a 180,60 0 1 1 -360,0"
       sodipodi:start="0"
       sodipodi:end="3.1415927"
       sodipodi:open="true" />
  </g>
</svg>